diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d6bc11..46b75db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,4 @@ +--- on: push: branches: @@ -6,6 +7,9 @@ on: branches: - master +env: + GO_VERSION: 1.18.x + name: run tests jobs: lint: @@ -14,7 +18,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: ${{ env.GO_VERSION }} - name: Checkout code uses: actions/checkout@v2 - name: Run linters @@ -35,7 +39,7 @@ jobs: if: success() uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: ${{ env.GO_VERSION }} - name: wait for RabbitMQ becoming ready run: timeout 30 sh -c "while true; do curl -s http://guest:password@localhost:15672/api/exchanges/%2f/amq.topic && break || sleep 3; done" @@ -48,15 +52,17 @@ jobs: with: github-token: ${{ secrets.github_token }} path-to-lcov: coverage.lcov - - name: Build release artifacts + if: env.build_artifacts # currently disabled uses: goreleaser/goreleaser-action@v3 with: version: latest args: build --rm-dist --snapshot env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload assets + if: env.build_artifacts uses: actions/upload-artifact@v3 with: name: rabtap-binaries diff --git a/CHANGELOG.md b/CHANGELOG.md index 77daec0..a2b6436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # Changelog for rabtap -## v1.38 (2022-09-02) - -* new: create exchange to exchange bindings with `rabtap exchange bind ...` +## v1.38 (2022-09-16) + +* new: create exchange-to-exchange bindings with `rabtap exchange bind ...` +* new: show exchange-to-exchange bindings in `info` command +* fix: drastically improve performance of `info` command for large topologies + with 1000's of queues/connections/channels +* chg: show channel information in `info` command with `--consumers` option +* chg: improve output of `info` command (attributes) +* chg: `dot` output of `info` command now shows separate vhosts ## v1.37 (2022-08-12) diff --git a/README.md b/README.md index 6a12a18..c24ca77 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,9 @@ Output of `rabtap info --stats` command, showing additional statistics: ### Visualize broker topology with graphviz Using the `--format=dot` option, the `info` command can generate output in the -`dot` format, which can be visualized using graphviz, e.g. `rabtap info ---show-default --format dot | dot -T svg > mybroker.svg`. The resulting SVG -file can be visualized with a web browser. +[dot format](https://graphviz.org/doc/info/lang.html), which can be visualized +using graphviz, e.g. `rabtap info --show-default --format dot | dot -T svg > +mybroker.svg`. The resulting SVG file can be visualized with a web browser. ![info mode](doc/images/info-dot.png) @@ -664,7 +664,7 @@ $ rabtap conn close '172.17.0.1:59228 -> 172.17.0.2:5672' #### Exchange commands -The `exchange` command is used to create and remove exchanges: +The `exchange` command is used to create, remove and bind exchanges: ```console $ rabtap exchange create myexchange --type topic diff --git a/cmd/rabtap/broker_info_renderer_dot.go b/cmd/rabtap/broker_info_renderer_dot.go index de84830..7695c92 100644 --- a/cmd/rabtap/broker_info_renderer_dot.go +++ b/cmd/rabtap/broker_info_renderer_dot.go @@ -6,6 +6,7 @@ package main import ( "fmt" + "html" "io" "net/url" "strconv" @@ -22,13 +23,14 @@ var ( // dotRendererTpl holds template fragments to use while rendering type dotRendererTpl struct { - dotTplRootNode string - dotTplVhost string - dotTplExchange string - dotTplQueue string - dotTplBoundQueue string - dotTplConsumer string - dotTplConnection string + dotTplRootNode string + dotTplVhost string + dotTplExchange string + dotTplBoundQueue string + dotTplQueueBinding string + dotTplConnection string + dotTplChannel string + dotTplConsumer string } // brokerInfoRendererDot renders into graphviz dot format @@ -43,17 +45,20 @@ type dotNode struct { ParentAssoc string } +var emptyDotNode = dotNode{} + // NewBrokerInfoRendererDot returns a BrokerInfoRenderer implementation that // renders into graphviz dot format func NewBrokerInfoRendererDot(config BrokerInfoRendererConfig) BrokerInfoRenderer { - return &brokerInfoRendererDot{config: config, template: newDotRendererTpl()} + return &brokerInfoRendererDot{ + config: config, template: newDotRendererTpl()} } // newDotRendererTpl returns the dot template to use. For now, just one default // template is used, later will support loading templates from the filesytem func newDotRendererTpl() dotRendererTpl { return dotRendererTpl{dotTplRootNode: `graph broker { -{{ q .Name }} [shape="record", label="{RabbitMQ {{ .Overview.RabbitmqVersion }} | +{{ q .Name }} [shape="record", label="{RabbitMQ {{ esc .Overview.RabbitmqVersion }} | {{- printf "%s://%s%s" .URL.Scheme .URL.Host .URL.Path }} | {{- .Overview.ClusterName }} }"]; @@ -61,31 +66,22 @@ func newDotRendererTpl() dotRendererTpl { {{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}} }`, - dotTplVhost: `{{ q .Name }} [shape="box", label="Virtual host {{ .Vhost }}"]; + dotTplVhost: `{{ q .Name }} [shape="box", label="Virtual host {{ esc .Vhost.Name }}"]; {{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name -}} [headport=n]{{ printf ";\n" }}{{ end -}} {{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, dotTplExchange: ` -{{ q .Name }} [shape="record"; label="{ {{ .Exchange.Name }} | {{- .Exchange.Type }} | { +{{ q .Name }} [shape="record"; label="{ {{ esc .Exchange.Name }} | {{- esc .Exchange.Type }} | { {{- if .Exchange.Durable }} D {{ end }} | {{- if .Exchange.AutoDelete }} AD {{ end }} | {{- if .Exchange.Internal }} I {{ end }} } }"]; -{{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }} [fontsize=10; headport=n; label={{ q $e.ParentAssoc }}]{{ printf ";\n" }}{{ end -}} -{{ range $i, $e := .Children }}{{ $e.Text }}{{ end -}}`, - - dotTplQueue: ` -{{ q .Name }} [shape="record"; label="{ {{ .Queue.Name }} | { - {{- if .Queue.Durable }} D {{ end }} | - {{- if .Queue.AutoDelete }} AD {{ end }} | - {{- if .Queue.Exclusive }} EX {{ end }} } }"]; - -{{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ printf ";\n" }}{{ end -}} +{{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }} [fontsize=10; headport=n; label={{ $e.ParentAssoc | esc | q}}]{{ printf ";\n" }}{{ end -}} {{ range $i, $e := .Children }}{{ $e.Text }}{{ end -}}`, dotTplBoundQueue: ` -{{ q .Name }} [shape="record"; label="{ {{ .Queue.Name }} | { +{{ q .Name }} [shape="record"; label="{ {{ esc .Queue.Name }} | { {{- if .Queue.Durable }} D {{ end }} | {{- if .Queue.AutoDelete }} AD {{ end }} | {{- if .Queue.Exclusive }} EX {{ end }} } }"]; @@ -93,138 +89,169 @@ func newDotRendererTpl() dotRendererTpl { {{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ end -}} {{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, - // TODO add more details + dotTplQueueBinding: ` +{{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ end -}} +{{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, + dotTplConnection: ` -{{ q .Name }} [shape="record" label="{{ .Connection.Name }}"]; +{{ q .Name }} [shape="record" label="{{ esc .Connection.Name }}"]; + +{{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ end -}} +{{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, + + dotTplChannel: ` +{{ q .Name }} [shape="record" label="{{ esc .Channel.Name }}"]; {{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ end -}} {{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, - // TODO add more details dotTplConsumer: ` -{{ q .Name }} [shape="record" label="{{ .Consumer.ConsumerTag}}"]; +{{ q .Name }} [shape="record" label="{{ esc .Consumer.ConsumerTag}}"]; {{ range $i, $e := .Children }}{{ q $.Name }} -- {{ q $e.Name }}{{ end -}} {{ range $i, $e := .Children }}{{ $e.Text -}}{{ end -}}`, } } -func (s brokerInfoRendererDot) renderRootNodeAsString(name string, children []dotNode, rabbitURL *url.URL, overview rabtap.RabbitOverview) string { +func (s brokerInfoRendererDot) funcMap() map[string]interface{} { + return map[string]interface{}{ + "q": strconv.Quote, + "esc": html.EscapeString} +} + +func (s brokerInfoRendererDot) renderRootNodeAsString(name string, + children []dotNode, + rabbitURL *url.URL, + overview *rabtap.RabbitOverview) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig URL *url.URL - Overview rabtap.RabbitOverview + Overview *rabtap.RabbitOverview }{name, children, s.config, rabbitURL, overview} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("root-dotTpl", s.template.dotTplRootNode, args, funcMap) + return resolveTemplate("root-dotTpl", s.template.dotTplRootNode, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderVhostAsString(name string, children []dotNode, vhost string) string { +func (s brokerInfoRendererDot) renderVhostAsString(name string, + children []dotNode, + vhost *rabtap.RabbitVhost) string { var args = struct { Name string Children []dotNode - Vhost string + Vhost *rabtap.RabbitVhost }{name, children, vhost} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("vhost-dotTpl", s.template.dotTplVhost, args, funcMap) + return resolveTemplate("vhost-dotTpl", s.template.dotTplVhost, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderExchangeElementAsString(name string, children []dotNode, exchange rabtap.RabbitExchange) string { +func (s brokerInfoRendererDot) renderExchangeElementAsString(name string, + children []dotNode, + exchange *rabtap.RabbitExchange) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig - Exchange rabtap.RabbitExchange + Exchange *rabtap.RabbitExchange }{name, children, s.config, exchange} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("exchange-dotTpl", s.template.dotTplExchange, args, funcMap) + return resolveTemplate("exchange-dotTpl", s.template.dotTplExchange, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderQueueElementAsString(name string, children []dotNode, queue rabtap.RabbitQueue) string { +func (s brokerInfoRendererDot) renderBoundQueueElementAsString(name string, + children []dotNode, + queue *rabtap.RabbitQueue, + binding *rabtap.RabbitBinding) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig - Queue rabtap.RabbitQueue - }{name, children, s.config, queue} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("queue-dotTpl", s.template.dotTplQueue, args, funcMap) + Binding *rabtap.RabbitBinding + Queue *rabtap.RabbitQueue + }{name, children, s.config, binding, queue} + return resolveTemplate("bound-queue-dotTpl", s.template.dotTplBoundQueue, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderBoundQueueElementAsString(name string, children []dotNode, queue rabtap.RabbitQueue, binding rabtap.RabbitBinding) string { +func (s brokerInfoRendererDot) renderConsumerElementAsString(name string, + children []dotNode, + consumer *rabtap.RabbitConsumer) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig - Binding rabtap.RabbitBinding - Queue rabtap.RabbitQueue - }{name, children, s.config, binding, queue} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("bound-queue-dotTpl", s.template.dotTplBoundQueue, args, funcMap) + Consumer *rabtap.RabbitConsumer + }{name, children, s.config, consumer} + return resolveTemplate("consumer-dotTpl", s.template.dotTplConsumer, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderConsumerElementAsString(name string, children []dotNode, consumer rabtap.RabbitConsumer) string { + +func (s brokerInfoRendererDot) renderChannelElementAsString(name string, + children []dotNode, + channel *rabtap.RabbitChannel) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig - Consumer rabtap.RabbitConsumer - }{name, children, s.config, consumer} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("consumer-dotTpl", s.template.dotTplConsumer, args, funcMap) + Channel *rabtap.RabbitChannel + }{name, children, s.config, channel} + return resolveTemplate("channel-dotTpl", s.template.dotTplChannel, args, s.funcMap()) } -func (s brokerInfoRendererDot) renderConnectionElementAsString(name string, children []dotNode, conn rabtap.RabbitConnection) string { +func (s brokerInfoRendererDot) renderConnectionElementAsString(name string, + children []dotNode, + conn *rabtap.RabbitConnection) string { var args = struct { Name string Children []dotNode Config BrokerInfoRendererConfig - Connection rabtap.RabbitConnection + Connection *rabtap.RabbitConnection }{name, children, s.config, conn} - funcMap := map[string]interface{}{"q": strconv.Quote} - return resolveTemplate("connnection-dotTpl", s.template.dotTplConnection, args, funcMap) + return resolveTemplate("connnection-dotTpl", s.template.dotTplConnection, args, s.funcMap()) } -func (s *brokerInfoRendererDot) renderNode(n interface{}) dotNode { +func (s *brokerInfoRendererDot) renderNode(n interface{}, queueRendered map[string]bool) dotNode { var node dotNode - + // render queues only once (otherwise in exchange-to-exchange binding + // scenarios, queues would be rendered multiple times) children := []dotNode{} for _, child := range n.(Node).Children() { - c := s.renderNode(child.(Node)) - children = append(children, c) + c := s.renderNode(child.(Node), queueRendered) + if c != emptyDotNode { + children = append(children, c) + } } switch t := n.(type) { case *rootNode: name := "root" - node = dotNode{name, s.renderRootNodeAsString(name, children, n.(*rootNode).URL, n.(*rootNode).Overview), ""} + node = dotNode{name, s.renderRootNodeAsString(name, children, t.URL, t.Overview), ""} case *vhostNode: - vhost := n.(*vhostNode) - name := fmt.Sprintf("vhost_%s", vhost.Vhost) - node = dotNode{name, s.renderVhostAsString(name, children, vhost.Vhost), ""} + name := fmt.Sprintf("vhost_%s", t.Vhost.Name) + node = dotNode{name, s.renderVhostAsString(name, children, t.Vhost), ""} case *exchangeNode: - exchange := n.(*exchangeNode).Exchange - name := fmt.Sprintf("exchange_%s", exchange.Name) - node = dotNode{name, s.renderExchangeElementAsString(name, children, exchange), ""} + name := fmt.Sprintf("exchange_%s_%s", t.Exchange.Vhost, t.Exchange.Name) + binding := t.OptBinding + key := "" + if binding != nil { + key = binding.RoutingKey + } + node = dotNode{name, s.renderExchangeElementAsString(name, children, t.Exchange), key} case *queueNode: - queue := n.(*queueNode).Queue - name := fmt.Sprintf("queue_%s", queue.Name) - node = dotNode{name, s.renderQueueElementAsString(name, children, queue), ""} - case *boundQueueNode: - boundQueue := n.(*boundQueueNode) - queue := boundQueue.Queue - binding := boundQueue.Binding - name := fmt.Sprintf("boundqueue_%s", queue.Name) - node = dotNode{name, s.renderBoundQueueElementAsString(name, children, queue, binding), binding.RoutingKey} + queue := t.Queue + name := fmt.Sprintf("queue_%s_%s", queue.Vhost, queue.Name) + queueRendered[name] = true + binding := t.OptBinding + key := "" + if binding != nil { + key = binding.RoutingKey + } + node = dotNode{name, s.renderBoundQueueElementAsString(name, children, queue, binding), key} case *connectionNode: - conn := n.(*connectionNode) - name := fmt.Sprintf("connection_%s", conn.Connection.Name) - node = dotNode{name, s.renderConnectionElementAsString(name, children, conn.Connection), ""} + name := fmt.Sprintf("connection_%s", t.Connection.Name) + node = dotNode{name, s.renderConnectionElementAsString(name, children, t.Connection), ""} + case *channelNode: + name := fmt.Sprintf("channel_%s", t.Channel.Name) + node = dotNode{name, s.renderChannelElementAsString(name, children, t.Channel), ""} case *consumerNode: - cons := n.(*consumerNode) - name := fmt.Sprintf("consumer_%s", cons.Consumer.ConsumerTag) - node = dotNode{name, s.renderConsumerElementAsString(name, children, cons.Consumer), ""} + name := fmt.Sprintf("consumer_%s", t.Consumer.ConsumerTag) + node = dotNode{name, s.renderConsumerElementAsString(name, children, t.Consumer), ""} default: panic(fmt.Sprintf("unexpected node encountered %T", t)) } @@ -234,7 +261,7 @@ func (s *brokerInfoRendererDot) renderNode(n interface{}) dotNode { // Render renders the given tree in graphviz dot format. See // https://www.graphviz.org/doc/info/lang.html func (s *brokerInfoRendererDot) Render(rootNode *rootNode, out io.Writer) error { - res := s.renderNode(rootNode) + res := s.renderNode(rootNode, map[string]bool{}) fmt.Fprintf(out, res.Text) return nil } diff --git a/cmd/rabtap/broker_info_renderer_text.go b/cmd/rabtap/broker_info_renderer_text.go index 4b1fd9b..a337818 100644 --- a/cmd/rabtap/broker_info_renderer_text.go +++ b/cmd/rabtap/broker_info_renderer_text.go @@ -47,143 +47,165 @@ const ( {{- "" }} mgmt ver='{{ .Overview.ManagementVersion }}', {{- "" }} cluster='{{ .Overview.ClusterName }}{{end}}')` tplVhost = ` - {{- printf "Vhost %s" .Vhost | VHostColor }}` - tplConsumer = ` - {{- ConsumerColor .Consumer.ConsumerTag }} (consumer - {{- ""}} user='{{ .Consumer.ChannelDetails.User }}', - {{- ""}} prefetch={{ .Consumer.PrefetchCount }}, chan=' - {{- .Consumer.ChannelDetails.Name }}')` + {{- printf "Vhost %s" .Vhost.Name | VHostColor }}` tplConnection = ` - {{- ""}}'{{ ConnectionColor .Connection.Name }}' (connection - {{- ""}} client='{{ .Connection.ClientProperties.Product}}', - {{- ""}} host='{{ .Connection.Host }}:{{ .Connection.Port }}', - {{- ""}} peer='{{ .Connection.PeerHost }}:{{ .Connection.PeerPort }}')` + {{- ""}}{{- if .NotFound }}{{ printf "? (connection %s)" .Connection.Name | ErrorColor }}{{else}} + {{- ""}}'{{ ConnectionColor .Connection.Name }}' + {{- ""}}{{- if .Connection.ClientProperties.ConnectionName }} ({{- .Connection.ClientProperties.ConnectionName }}) {{end}} + {{- ""}} (connection {{ .Connection.User}}@{{ .Connection.Host }}:{{ .Connection.Port }}, + {{- ""}} state='{{ .Connection.State }}', + {{- ""}} client='{{ .Connection.ClientProperties.Product }}', + {{- ""}} ver='{{ .Connection.ClientProperties.Version }}', + {{- ""}} peer='{{ .Connection.PeerHost }}:{{ .Connection.PeerPort }}') + {{- end}}` + tplChannel = ` + {{- ""}}{{- if .NotFound }}{{ printf "? (channel %s)" .Channel.Name | ErrorColor }}{{else}} + {{- ""}}'{{ ChannelColor .Channel.Name }}' (channel + {{- ""}} prefetch={{ .Channel.PrefetchCount }}, + {{- ""}} state={{ .Channel.State }}, + {{- ""}} unacked={{ .Channel.MessagesUnacknowledged }}, + {{- ""}} confirms={{ .Channel.Confirm | YesNo }} + {{- if .Channel.IdleSince}}{{- ", idle since "}}{{ .Channel.IdleSince}}{{else}} + {{- if and .Config.ShowStats .Channel.MessageStats }} ( + {{- ", "}} + {{- with .Channel.MessageStats.PublishDetails }}{{ if gt .Rate 0. }}{{printf "pub=%.1f" .Rate}}{{end}}{{end}} + {{- with .Channel.MessageStats.ConfirmDetails }}{{ if gt .Rate 0. }}{{printf ", confirms=%.1f " .Rate}}{{end}}{{end}} + {{- with .Channel.MessageStats.ReturnUnroutableDetails }}{{ if gt .Rate 0. }}{{printf ", drop=%.1f " .Rate}}{{end}}{{end}} + {{- with .Channel.MessageStats.DeliverGetDetails }}{{ if gt .Rate 0. }}{{printf "get=%.1f" .Rate}}{{end}}{{end}} + {{- with .Channel.MessageStats.AckDetails }}{{ if gt .Rate 0. }}{{printf ", ack=%.1f" .Rate}}{{end}}{{end}}) msg/s + {{- end }} + {{- end}}) + {{- end}}` + tplConsumer = ` + {{- ConsumerColor .Consumer.ConsumerTag }} (consumer + {{- ""}} prefetch={{ .Consumer.PrefetchCount }}, + {{- ""}} ack_req={{ .Consumer.AckRequired | YesNo }}, + {{- ""}} active={{ .Consumer.Active | YesNo }}, + {{- ""}} status={{ .Consumer.ActivityStatus }})` tplExchange = ` {{- if eq .Exchange.Name "" }}{{ ExchangeColor "(default)" }}{{ else }}{{ ExchangeColor .Exchange.Name }}{{ end }} - {{- "" }} (exchange, type '{{ .Exchange.Type }}' - {{- if and .Config.ShowStats .Exchange.MessageStats }}, in=( - {{- .Exchange.MessageStats.PublishIn }}, {{printf "%.1f" .Exchange.MessageStats.PublishInDetails.Rate}}/s) msg, out=( - {{- .Exchange.MessageStats.PublishOut }}, {{printf "%.1f" .Exchange.MessageStats.PublishOutDetails.Rate}}/s) msg - {{- end }}, [{{ .ExchangeFlags }}])` - tplQueue = ` - {{- QueueColor .Queue.Name }} (queue({{ .Queue.Type}}), - {{- if .Config.ShowStats }} - {{- .Queue.Consumers }} cons, ( - {{- .Queue.Messages }}, {{printf "%.1f" .Queue.MessagesDetails.Rate}}/s) msg, ( - {{- .Queue.MessagesReady }}, {{printf "%.1f" .Queue.MessagesReadyDetails.Rate}}/s) msg ready, - {{- " " }}{{ ToPercent .Queue.ConsumerUtilisation }}% utl, + {{- "" }} (exchange({{ .Exchange.Type }}), + {{- if .Binding }} + {{- with .Binding.RoutingKey }} key='{{ KeyColor .}}',{{end}} + {{- with .Binding.Arguments}} args='{{ KeyColor .}}',{{end}} {{- end }} - {{- if .Queue.IdleSince}}{{- " idle since "}}{{ .Queue.IdleSince}}{{else}}{{ " running" }}{{end}} - {{- ""}}, [{{ .QueueFlags}}])` + {{- if and .Config.ShowStats .Exchange.MessageStats }} in=( + {{- .Exchange.MessageStats.PublishIn }}, {{printf "%.1f" .Exchange.MessageStats.PublishInDetails.Rate}}/s) msg, out=( + {{- .Exchange.MessageStats.PublishOut }}, {{printf "%.1f" .Exchange.MessageStats.PublishOutDetails.Rate}}/s) msg, + {{- end }} [{{ .ExchangeFlags }}])` tplBoundQueue = ` - {{- QueueColor .Binding.Destination }} (queue({{ .Queue.Type}}), - {{- with .Binding.RoutingKey }} key='{{ KeyColor .}}',{{end}} - {{- with .Binding.Arguments}} args='{{ KeyColor .}}',{{end}} + {{- QueueColor .Queue.Name }} (queue({{ .Queue.Type}}), + {{- if .Binding }} + {{- with .Binding.RoutingKey }} key='{{ KeyColor .}}',{{end}} + {{- with .Binding.Arguments}} args='{{ KeyColor .}}',{{end}} + {{- end }} + {{- " " }} {{- if .Config.ShowStats }} - {{- .Queue.Consumers }} cons, ( - {{- .Queue.Messages }}, {{printf "%.1f" .Queue.MessagesDetails.Rate}}/s) msg, ( - {{- .Queue.MessagesReady }}, {{printf "%.1f" .Queue.MessagesReadyDetails.Rate}}/s) msg ready, - {{- " " }}{{ ToPercent .Queue.ConsumerUtilisation }}% utl, + {{- .Queue.Consumers }} cons, ( + {{- .Queue.Messages }}, {{printf "%.1f" .Queue.MessagesDetails.Rate}}/s) msg, ( + {{- .Queue.MessagesReady }}, {{printf "%.1f" .Queue.MessagesReadyDetails.Rate}}/s) msg ready, + {{- " " }}{{ ToPercent .Queue.ConsumerUtilisation }}% utl, {{- end }} {{- if .Queue.IdleSince}}{{- " idle since "}}{{ .Queue.IdleSince}}{{else}}{{ " running" }}{{end}} {{- ""}}, [{{ .QueueFlags}}])` ) -func (s brokerInfoRendererText) renderQueueFlagsAsString(queue rabtap.RabbitQueue) string { +func (s brokerInfoRendererText) renderQueueFlagsAsString(queue *rabtap.RabbitQueue) string { flags := []bool{queue.Durable, queue.AutoDelete, queue.Exclusive} names := []string{"D", "AD", "EX"} return strings.Join(filterStringList(flags, names), "|") } -func (s brokerInfoRendererText) renderExchangeFlagsAsString(exchange rabtap.RabbitExchange) string { +func (s brokerInfoRendererText) renderExchangeFlagsAsString(exchange *rabtap.RabbitExchange) string { flags := []bool{exchange.Durable, exchange.AutoDelete, exchange.Internal} names := []string{"D", "AD", "I"} return strings.Join(filterStringList(flags, names), "|") } -func (s brokerInfoRendererText) renderVhostAsString(vhost string) string { +func (s brokerInfoRendererText) renderVhostAsString(vhost *rabtap.RabbitVhost) string { var args = struct { - Vhost string + Vhost *rabtap.RabbitVhost }{vhost} return resolveTemplate("vhost-tpl", tplVhost, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderConsumerElementAsString(consumer rabtap.RabbitConsumer) string { +func (s brokerInfoRendererText) renderConsumerElementAsString(consumer *rabtap.RabbitConsumer) string { var args = struct { Config BrokerInfoRendererConfig - Consumer rabtap.RabbitConsumer + Consumer *rabtap.RabbitConsumer }{s.config, consumer} return resolveTemplate("consumer-tpl", tplConsumer, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderConnectionElementAsString(conn rabtap.RabbitConnection) string { +func (s brokerInfoRendererText) renderConnectionElementAsString(conn *rabtap.RabbitConnection, status nodeStatus) string { var args = struct { Config BrokerInfoRendererConfig - Connection rabtap.RabbitConnection - }{s.config, conn} + Connection *rabtap.RabbitConnection + NotFound bool + }{s.config, conn, status == NotFound} return resolveTemplate("connnection-tpl", tplConnection, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderQueueElementAsString(queue rabtap.RabbitQueue) string { - queueFlags := s.renderQueueFlagsAsString(queue) +func (s brokerInfoRendererText) renderChannelElementAsString(channel *rabtap.RabbitChannel, status nodeStatus) string { var args = struct { - Config BrokerInfoRendererConfig - Queue rabtap.RabbitQueue - QueueFlags string - }{s.config, queue, queueFlags} - return resolveTemplate("queue-tpl", tplQueue, args, s.templateFuncs) + Config BrokerInfoRendererConfig + Channel *rabtap.RabbitChannel + NotFound bool + }{s.config, channel, status == NotFound} + return resolveTemplate("channel-tpl", tplChannel, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderBoundQueueElementAsString(queue rabtap.RabbitQueue, binding rabtap.RabbitBinding) string { +func (s brokerInfoRendererText) renderBoundQueueElementAsString(queue *rabtap.RabbitQueue, binding *rabtap.RabbitBinding) string { queueFlags := s.renderQueueFlagsAsString(queue) var args = struct { Config BrokerInfoRendererConfig - Binding rabtap.RabbitBinding - Queue rabtap.RabbitQueue + Binding *rabtap.RabbitBinding + Queue *rabtap.RabbitQueue QueueFlags string }{s.config, binding, queue, queueFlags} return resolveTemplate("bound-queue-tpl", tplBoundQueue, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderRootNodeAsString(rabbitURL *url.URL, overview rabtap.RabbitOverview) string { +func (s brokerInfoRendererText) renderRootNodeAsString(rabbitURL *url.URL, overview *rabtap.RabbitOverview) string { var args = struct { Config BrokerInfoRendererConfig URL *url.URL - Overview rabtap.RabbitOverview + Overview *rabtap.RabbitOverview }{s.config, rabbitURL, overview} return resolveTemplate("rootnode", tplRootNode, args, s.templateFuncs) } -func (s brokerInfoRendererText) renderExchangeElementAsString(exchange rabtap.RabbitExchange) string { +func (s brokerInfoRendererText) renderExchangeElementAsString(exchange *rabtap.RabbitExchange, binding *rabtap.RabbitBinding) string { exchangeFlags := s.renderExchangeFlagsAsString(exchange) var args = struct { Config BrokerInfoRendererConfig - Exchange rabtap.RabbitExchange + Exchange *rabtap.RabbitExchange ExchangeFlags string - }{s.config, exchange, exchangeFlags} + Binding *rabtap.RabbitBinding + }{s.config, exchange, exchangeFlags, binding} return resolveTemplate("exchange-tpl", tplExchange, args, s.templateFuncs) } func (s brokerInfoRendererText) renderNode(n interface{}) *TreeNode { var node *TreeNode - switch t := n.(type) { + switch e := n.(type) { case *rootNode: - node = NewTreeNode(s.renderRootNodeAsString(n.(*rootNode).URL, n.(*rootNode).Overview)) + node = NewTreeNode(s.renderRootNodeAsString(e.URL, e.Overview)) case *vhostNode: - node = NewTreeNode(s.renderVhostAsString(n.(*vhostNode).Vhost)) + node = NewTreeNode(s.renderVhostAsString(e.Vhost)) case *connectionNode: - node = NewTreeNode(s.renderConnectionElementAsString(n.(*connectionNode).Connection)) + node = NewTreeNode(s.renderConnectionElementAsString(e.Connection, e.Status)) + case *channelNode: + node = NewTreeNode(s.renderChannelElementAsString(e.Channel, e.Status)) case *consumerNode: - node = NewTreeNode(s.renderConsumerElementAsString(n.(*consumerNode).Consumer)) + node = NewTreeNode(s.renderConsumerElementAsString(e.Consumer)) case *queueNode: - node = NewTreeNode(s.renderQueueElementAsString(n.(*queueNode).Queue)) - case *boundQueueNode: - node = NewTreeNode(s.renderBoundQueueElementAsString(n.(*boundQueueNode).Queue, n.(*boundQueueNode).Binding)) + node = NewTreeNode(s.renderBoundQueueElementAsString(e.Queue, e.OptBinding)) case *exchangeNode: - node = NewTreeNode(s.renderExchangeElementAsString(n.(*exchangeNode).Exchange)) + node = NewTreeNode(s.renderExchangeElementAsString(e.Exchange, e.OptBinding)) default: - panic(fmt.Sprintf("unexpected node encountered %T", t)) + panic(fmt.Sprintf("unexpected node encountered %T", e)) } for _, child := range n.(Node).Children() { diff --git a/cmd/rabtap/broker_info_tree_builder.go b/cmd/rabtap/broker_info_tree_builder.go index 4e40016..008c877 100644 --- a/cmd/rabtap/broker_info_tree_builder.go +++ b/cmd/rabtap/broker_info_tree_builder.go @@ -3,8 +3,6 @@ // rendering of info return by broker API into abstract tree represenations, // which can later be rendered into something useful (e.g. text, dot etc.) // Definition of interface and default implementation. -// TODO split into interface, impl and factory when new builder(s) are -// implemented // TODO add unit test. currently only component tested in cmd_info_test.go package main @@ -12,6 +10,7 @@ package main import ( "fmt" "net/url" + "sort" rabtap "github.com/jandelgado/rabtap/pkg" ) @@ -22,14 +21,14 @@ type BrokerInfoTreeBuilderConfig struct { ShowDefaultExchange bool ShowConsumers bool ShowStats bool - QueueFilter Predicate + Filter Predicate OmitEmptyExchanges bool } // BrokerInfoTreeBuilder transforms a rabtap.BrokerInfo into a tree // representation that can be easily rendered (e.g. into text, dot fomats) type BrokerInfoTreeBuilder interface { - BuildTree(rootNodeURL *url.URL, brokerInfo rabtap.BrokerInfo) (*rootNode, error) + BuildTree(rootNodeURL *url.URL, metadataService rabtap.MetadataService) (*rootNode, error) } type brokerInfoTreeBuilderByConnection struct{ config BrokerInfoTreeBuilderConfig } @@ -67,51 +66,85 @@ func (s *baseNode) Children() []interface{} { return s.children } -func (s *baseNode) HasChildren() bool { +func (s *baseNode) hasChildren() bool { return len(s.children) > 0 } type rootNode struct { baseNode - Overview rabtap.RabbitOverview + Overview *rabtap.RabbitOverview URL *url.URL } type vhostNode struct { baseNode - Vhost string + Vhost *rabtap.RabbitVhost +} + +func newVhostNode(vhost *rabtap.RabbitVhost) *vhostNode { + return &vhostNode{baseNode{[]interface{}{}}, vhost} } type exchangeNode struct { baseNode - Exchange rabtap.RabbitExchange + Exchange *rabtap.RabbitExchange + OptBinding *rabtap.RabbitBinding // optional binding in case of e-to-e binding +} + +func newExchangeNode(exchange *rabtap.RabbitExchange, binding *rabtap.RabbitBinding) *exchangeNode { + return &exchangeNode{baseNode{[]interface{}{}}, exchange, binding} } type queueNode struct { baseNode - Queue rabtap.RabbitQueue + Queue *rabtap.RabbitQueue + OptBinding *rabtap.RabbitBinding // optional binding if queue is bound to exchange } -type boundQueueNode struct { - baseNode - Queue rabtap.RabbitQueue - Binding rabtap.RabbitBinding +func newQueueNode(queue *rabtap.RabbitQueue) *queueNode { + return &queueNode{baseNode{[]interface{}{}}, queue, nil} +} + +func newQueueNodeWithBinding(queue *rabtap.RabbitQueue, binding *rabtap.RabbitBinding) *queueNode { + return &queueNode{baseNode{[]interface{}{}}, queue, binding} } +type nodeStatus int + +const ( + // object was found and contains actual data + Valid nodeStatus = iota + // object was not found and usually contains only the name attribute set + NotFound +) + type connectionNode struct { baseNode - Connection rabtap.RabbitConnection + Connection *rabtap.RabbitConnection + Status nodeStatus } -// channelNode is not yet used -// type channelNode struct { -// baseNode -// Channel rabtap.RabbitConnection -// } +func newConnectionNode(connection *rabtap.RabbitConnection, status nodeStatus) *connectionNode { + return &connectionNode{baseNode{[]interface{}{}}, connection, status} +} + +type channelNode struct { + baseNode + Channel *rabtap.RabbitChannel + Status nodeStatus +} + +func newChannelNode(channel *rabtap.RabbitChannel, status nodeStatus) *channelNode { + return &channelNode{baseNode{[]interface{}{}}, channel, status} +} type consumerNode struct { baseNode - Consumer rabtap.RabbitConsumer + Consumer *rabtap.RabbitConsumer +} + +func newConsumerNode(consumer *rabtap.RabbitConsumer) *consumerNode { + return &consumerNode{baseNode{[]interface{}{}}, consumer} } type defaultBrokerInfoTreeBuilder struct { @@ -123,9 +156,9 @@ func newDefaultBrokerInfoTreeBuilder(config BrokerInfoTreeBuilderConfig) *defaul } func (s defaultBrokerInfoTreeBuilder) shouldDisplayExchange( - exchange rabtap.RabbitExchange, vhost string) bool { + exchange *rabtap.RabbitExchange, vhost *rabtap.RabbitVhost) bool { - if exchange.Vhost != vhost { + if exchange.Vhost != vhost.Name { return false } if exchange.Name == "" && !s.config.ShowDefaultExchange { @@ -135,13 +168,23 @@ func (s defaultBrokerInfoTreeBuilder) shouldDisplayExchange( return true } +// orderedKeySet returns the key set of the given map as a sorted array of strings +func orderedKeySet[T any](m map[string]T) []string { + var keys []string + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + func (s defaultBrokerInfoTreeBuilder) shouldDisplayQueue( - queue rabtap.RabbitQueue, - exchange rabtap.RabbitExchange, - binding rabtap.RabbitBinding) bool { + queue *rabtap.RabbitQueue, + exchange *rabtap.RabbitExchange, + binding *rabtap.RabbitBinding) bool { params := map[string]interface{}{"queue": queue, "binding": binding, "exchange": exchange} - if res, err := s.config.QueueFilter.Eval(params); err != nil || !res { + if res, err := s.config.Filter.Eval(params); err != nil || !res { if err != nil { log.Warnf("error evaluating queue filter: %s", err) } else { @@ -151,117 +194,143 @@ func (s defaultBrokerInfoTreeBuilder) shouldDisplayQueue( return true } +// createConnectionNodes creates a tree for the given queue with all +// connections -> channels -> consumers consuming from this queue. func (s defaultBrokerInfoTreeBuilder) createConnectionNodes( - vhost string, connName string, brokerInfo rabtap.BrokerInfo) []*connectionNode { - var conns []*connectionNode - i := rabtap.FindConnectionByName(brokerInfo.Connections, vhost, connName) - if i != -1 { - return []*connectionNode{{baseNode{[]interface{}{}}, brokerInfo.Connections[i]}} - } - return conns -} - -func (s defaultBrokerInfoTreeBuilder) createConsumerNodes( - queue rabtap.RabbitQueue, brokerInfo rabtap.BrokerInfo) []*consumerNode { - var nodes []*consumerNode - vhost := queue.Vhost - for _, consumer := range brokerInfo.Consumers { - if consumer.Queue.Vhost == vhost && - consumer.Queue.Name == queue.Name { - consumerNode := consumerNode{baseNode{[]interface{}{}}, consumer} - connectionNodes := s.createConnectionNodes(vhost, consumer.ChannelDetails.ConnectionName, brokerInfo) - for _, connectionNode := range connectionNodes { - consumerNode.Add(connectionNode) + queue *rabtap.RabbitQueue, + metadataService rabtap.MetadataService) []*connectionNode { + + connectionNodes := map[string]*connectionNode{} + channelNodes := map[string]*channelNode{} + + vhostName := queue.Vhost + for _, consumer := range metadataService.Consumers() { // TODO AllConsumersByQueue + if !(consumer.Queue.Vhost == vhostName && consumer.Queue.Name == queue.Name) { + continue + } + consumerNode := newConsumerNode(&consumer) + + var ok bool + + connectionName := consumer.ChannelDetails.ConnectionName + var connNode *connectionNode + if connNode, ok = connectionNodes[connectionName]; !ok { + connection := metadataService.FindConnectionByName(vhostName, connectionName) + if connection != nil { + connNode = newConnectionNode(connection, Valid) + } else { + // for some reason, the connection could not be found by it's name. + // So we create an empty connection object and mark it as "Not found" + // and let the renderer decide what to do. + dummyConnection := rabtap.RabbitConnection{Name: connectionName} + connNode = newConnectionNode(&dummyConnection, NotFound) } - nodes = append(nodes, &consumerNode) + connectionNodes[connectionName] = connNode } + + channelName := consumer.ChannelDetails.Name + var chanNode *channelNode + if chanNode, ok = channelNodes[channelName]; !ok { + channel := metadataService.FindChannelByName(vhostName, channelName) + if channel != nil { + chanNode = newChannelNode(channel, Valid) + } else { + dummyChan := rabtap.RabbitChannel{Name: channelName} + chanNode = newChannelNode(&dummyChan, NotFound) + } + channelNodes[channelName] = chanNode + } + + connNode.Add(chanNode) + chanNode.Add(consumerNode) + } + + var nodes []*connectionNode + for _, key := range orderedKeySet(connectionNodes) { + nodes = append(nodes, connectionNodes[key]) } return nodes } func (s defaultBrokerInfoTreeBuilder) createQueueNodeFromBinding( - binding rabtap.RabbitBinding, - exchange rabtap.RabbitExchange, - brokerInfo rabtap.BrokerInfo) []*boundQueueNode { + binding *rabtap.RabbitBinding, + exchange *rabtap.RabbitExchange, + metadataService rabtap.MetadataService) []*queueNode { // standard binding of queue to exchange - i := rabtap.FindQueueByName(brokerInfo.Queues, - binding.Vhost, - binding.Destination) - - queue := rabtap.RabbitQueue{Name: binding.Destination} // default in case not found - if i != -1 { - // we test for -1 because (at least in theory) a queue can disappear - // since we are making various non-transactional API calls - queue = brokerInfo.Queues[i] + queue := metadataService.FindQueueByName(binding.Vhost, binding.Destination) + + if queue == nil { + // can happen in theory since REST calls are not transactional + return []*queueNode{} } if !s.shouldDisplayQueue(queue, exchange, binding) { - return []*boundQueueNode{} + return []*queueNode{} } - queueNode := boundQueueNode{baseNode{[]interface{}{}}, queue, binding} + node := newQueueNodeWithBinding(queue, binding) if s.config.ShowConsumers { - consumers := s.createConsumerNodes(queue, brokerInfo) + consumers := s.createConnectionNodes(queue, metadataService) for _, consumer := range consumers { - queueNode.Add(consumer) + node.Add(consumer) } } - return []*boundQueueNode{&queueNode} + return []*queueNode{node} } // createExchangeNode recursively (in case of exchange-exchange binding) an // exchange to the given node. func (s defaultBrokerInfoTreeBuilder) createExchangeNode( - exchange rabtap.RabbitExchange, brokerInfo rabtap.BrokerInfo) *exchangeNode { + exchange *rabtap.RabbitExchange, + metadataService rabtap.MetadataService, + binding *rabtap.RabbitBinding) *exchangeNode { // to detect cyclic exchange-to-exchange bindings. Yes, this is possible. visited := map[string]bool{} - var create func(rabtap.RabbitExchange, rabtap.BrokerInfo) *exchangeNode - create = func(exchange rabtap.RabbitExchange, brokerInfo rabtap.BrokerInfo) *exchangeNode { + var create func(*rabtap.RabbitExchange, rabtap.MetadataService, *rabtap.RabbitBinding) *exchangeNode + create = func(exchange *rabtap.RabbitExchange, metadataService rabtap.MetadataService, binding *rabtap.RabbitBinding) *exchangeNode { - exchangeNode := exchangeNode{baseNode{[]interface{}{}}, exchange} + exchangeNode := newExchangeNode(exchange, binding) - // process all bindings for current exchange - for _, binding := range rabtap.FindBindingsForExchange(exchange, brokerInfo.Bindings) { - if binding.DestinationType == "exchange" { - // exchange to exchange binding - i := rabtap.FindExchangeByName( - brokerInfo.Exchanges, - binding.Vhost, - binding.Destination) - if i == -1 { + // process all bindings for current exchange. Can be exchange-exchange- + // as well as queue-to-exchange bindings. + //for _, binding := range rabtap.FindBindingsForExchange(exchange, brokerInfo.Bindings) { + for _, binding := range metadataService.AllBindingsForExchange(exchange.Vhost, exchange.Name) { + if binding.IsExchangeToExchange() { + boundExchange := metadataService.FindExchangeByName(binding.Vhost, binding.Destination) + if boundExchange == nil { // ignore if not found continue } - boundExchange := brokerInfo.Exchanges[i] if _, found := visited[boundExchange.Name]; found { // cyclic exchange-to-exchange binding detected continue } visited[boundExchange.Name] = true - exchangeNode.Add(create(boundExchange, brokerInfo)) + exchangeNode.Add(create(boundExchange, metadataService, binding)) } else { - // do not add (redundant) queues if in recursive exchange call + // do not add (redundant) queues if in recursive exchange-to-exchange + // binding: show queues only below top-level exchange if len(visited) > 0 { continue } // queue to exchange binding - queues := s.createQueueNodeFromBinding(binding, exchange, brokerInfo) + queues := s.createQueueNodeFromBinding(binding, exchange, metadataService) for _, queue := range queues { exchangeNode.Add(queue) } } } - return &exchangeNode + return exchangeNode } - return create(exchange, brokerInfo) + return create(exchange, metadataService, binding) } func (s defaultBrokerInfoTreeBuilder) createRootNode(rootNodeURL *url.URL, - overview rabtap.RabbitOverview) *rootNode { + overview *rabtap.RabbitOverview) *rootNode { b := baseNode{[]interface{}{}} return &rootNode{b, overview, rootNodeURL} } @@ -271,28 +340,32 @@ func (s defaultBrokerInfoTreeBuilder) createRootNode(rootNodeURL *url.URL, // +--VHost // +--Exchange // +--Queue bound to exchange -// +--Consumer (optional) -// +--Connection +// +--Connection (optional) +// +--Channel +// +--Consumer // -func (s defaultBrokerInfoTreeBuilder) buildTreeByExchange(rootNodeURL *url.URL, - brokerInfo rabtap.BrokerInfo) (*rootNode, error) { +func (s defaultBrokerInfoTreeBuilder) buildTreeByExchange( + rootNodeURL *url.URL, + metadataService rabtap.MetadataService) (*rootNode, error) { - b := baseNode{[]interface{}{}} - rootNode := s.createRootNode(rootNodeURL, brokerInfo.Overview) + overview := metadataService.Overview() + rootNode := s.createRootNode(rootNodeURL, &overview) - for vhost := range rabtap.UniqueVhosts(brokerInfo.Exchanges) { - vhostNode := vhostNode{b, vhost} - for _, exchange := range brokerInfo.Exchanges { - if !s.shouldDisplayExchange(exchange, vhost) { + for _, vhost := range metadataService.Vhosts() { + vhost := vhost + vhostNode := newVhostNode(&vhost) + for _, exchange := range metadataService.Exchanges() { + exchange := exchange + if !s.shouldDisplayExchange(&exchange, &vhost) { continue } - exNode := s.createExchangeNode(exchange, brokerInfo) - if s.config.OmitEmptyExchanges && !exNode.HasChildren() { + exNode := s.createExchangeNode(&exchange, metadataService, nil) + if s.config.OmitEmptyExchanges && !exNode.hasChildren() { continue } vhostNode.Add(exNode) } - rootNode.Add(&vhostNode) + rootNode.Add(vhostNode) } return rootNode, nil } @@ -301,54 +374,80 @@ func (s defaultBrokerInfoTreeBuilder) buildTreeByExchange(rootNodeURL *url.URL, // RabbitMQ-Host // +--VHost // +--Connection -// +--Consumer -// +--Queue -// TODO add filtering -func (s defaultBrokerInfoTreeBuilder) buildTreeByConnection(rootNodeURL *url.URL, - brokerInfo rabtap.BrokerInfo) (*rootNode, error) { +// +--Channel +// +--Consumer (opt) +// +--Queue +func (s defaultBrokerInfoTreeBuilder) buildTreeByConnection( + rootNodeURL *url.URL, + metadataService rabtap.MetadataService) (*rootNode, error) { + + overview := metadataService.Overview() + rootNode := s.createRootNode(rootNodeURL, &overview) + + vhosts := map[string]*vhostNode{} + for _, conn := range metadataService.Connections() { + conn := conn + vhostName := conn.Vhost + var ok bool + if _, ok = vhosts[vhostName]; !ok { + vhost := metadataService.FindVhostByName(vhostName) + vhosts[vhostName] = newVhostNode(vhost) + } - b := baseNode{[]interface{}{}} - rootNode := s.createRootNode(rootNodeURL, brokerInfo.Overview) - - for vhost := range rabtap.UniqueVhosts(brokerInfo.Exchanges) { - vhostNode := vhostNode{b, vhost} - for _, conn := range brokerInfo.Connections { - connNode := connectionNode{b, conn} - for _, consumer := range brokerInfo.Consumers { - if consumer.ChannelDetails.ConnectionName != conn.Name { - continue - } - consNode := consumerNode{b, consumer} - for _, queue := range brokerInfo.Queues { - if consumer.Queue.Vhost == vhost && consumer.Queue.Name == queue.Name { - queueNode := queueNode{b, queue} - consNode.Add(&queueNode) - } + connNode := newConnectionNode(&conn, Valid) + + channels := metadataService.AllChannelsForConnection(vhostName, conn.Name) + for _, channel := range channels { + channel := channel + + params := map[string]interface{}{"connection": conn, "channel": channel} + if res, err := s.config.Filter.Eval(params); err != nil || !res { + if err != nil { + log.Warnf("error evaluating queue filter: %s", err) } - connNode.Add(&consNode) - } - if s.config.OmitEmptyExchanges && !connNode.HasChildren() { continue } - vhostNode.Add(&connNode) + + chanNode := newChannelNode(channel, Valid) + + consumers := metadataService.AllConsumersForChannel(vhostName, channel.Name) + for _, consumer := range consumers { + consumer := consumer + consNode := newConsumerNode(consumer) + if queue := metadataService.FindQueueByName(vhostName, consumer.Queue.Name); queue != nil { + queueNode := newQueueNode(queue) + consNode.Add(queueNode) + } + chanNode.Add(consNode) + } + // if s.config.OmitEmptyExchanges && !connNode.HasChildren() { + // continue + // } + connNode.Add(chanNode) } - rootNode.Add(&vhostNode) + if connNode.hasChildren() { + vhosts[vhostName].Add(connNode) + } + } + + for _, key := range orderedKeySet(vhosts) { + rootNode.Add(vhosts[key]) } return rootNode, nil } func (s brokerInfoTreeBuilderByConnection) BuildTree( rootNodeURL *url.URL, - brokerInfo rabtap.BrokerInfo) (*rootNode, error) { + metadataService rabtap.MetadataService) (*rootNode, error) { builder := newDefaultBrokerInfoTreeBuilder(s.config) - return builder.buildTreeByConnection(rootNodeURL, brokerInfo) + return builder.buildTreeByConnection(rootNodeURL, metadataService) } func (s brokerInfoTreeBuilderByExchange) BuildTree( rootNodeURL *url.URL, - brokerInfo rabtap.BrokerInfo) (*rootNode, error) { + metadataService rabtap.MetadataService) (*rootNode, error) { builder := newDefaultBrokerInfoTreeBuilder(s.config) - return builder.buildTreeByExchange(rootNodeURL, brokerInfo) + return builder.buildTreeByExchange(rootNodeURL, metadataService) } diff --git a/cmd/rabtap/cmd_exchange_test.go b/cmd/rabtap/cmd_exchange_test.go index 52410cc..5edffca 100644 --- a/cmd/rabtap/cmd_exchange_test.go +++ b/cmd/rabtap/cmd_exchange_test.go @@ -1,5 +1,6 @@ // Copyright (C) 2017 Jan Delgado +//go:build integration // +build integration package main @@ -7,6 +8,7 @@ package main import ( "context" "crypto/tls" + "fmt" "net/url" "os" "testing" @@ -15,16 +17,20 @@ import ( rabtap "github.com/jandelgado/rabtap/pkg" "github.com/jandelgado/rabtap/pkg/testcommon" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -// exchangeExists queries the API to check if a given exchange exists -func exchangeExists(t *testing.T, apiURL *url.URL, exchange string) bool { - // TODO add a simple client to testcommon +func findExchangeByName(apiURL *url.URL, vhost, name string) (*rabtap.RabbitExchange, error) { client := rabtap.NewRabbitHTTPClient(apiURL, &tls.Config{}) exchanges, err := client.Exchanges(context.TODO()) - require.Nil(t, err) - return rabtap.FindExchangeByName(exchanges, "/", exchange) != -1 + if err != nil { + return nil, err + } + for _, e := range exchanges { + if e.Name == name && e.Vhost == vhost { + return &e, nil + } + } + return nil, fmt.Errorf("exchange not found") } func TestIntegrationCmdExchangeCreateRemoveExchange(t *testing.T) { @@ -37,16 +43,21 @@ func TestIntegrationCmdExchangeCreateRemoveExchange(t *testing.T) { amqpURL := testcommon.IntegrationURIFromEnv() apiURL, _ := url.Parse(testcommon.IntegrationAPIURIFromEnv()) - assert.False(t, exchangeExists(t, apiURL, testExchange)) + _, err := findExchangeByName(apiURL, "/", testExchange) + assert.Error(t, fmt.Errorf("exchange not found")) + os.Args = []string{"rabtap", "exchange", "create", testExchange, "--uri", amqpURL.String()} main() time.Sleep(2 * time.Second) - assert.True(t, exchangeExists(t, apiURL, testExchange)) + _, err = findExchangeByName(apiURL, "/", testExchange) + assert.NoError(t, err) // TODO validation os.Args = []string{"rabtap", "exchange", "rm", testExchange, "--uri", amqpURL.String()} main() time.Sleep(2 * time.Second) - assert.False(t, exchangeExists(t, apiURL, testExchange)) + + _, err = findExchangeByName(apiURL, "/", testExchange) + assert.Error(t, fmt.Errorf("exchange not found")) } diff --git a/cmd/rabtap/cmd_info.go b/cmd/rabtap/cmd_info.go index c865708..6c8d7f7 100644 --- a/cmd/rabtap/cmd_info.go +++ b/cmd/rabtap/cmd_info.go @@ -31,6 +31,7 @@ func cmdInfo(ctx context.Context, cmd CmdInfoArg) { failOnError(err, "failed instanciating tree builder", os.Exit) renderer := NewBrokerInfoRenderer(cmd.renderConfig) - tree, _ := treeBuilder.BuildTree(cmd.rootNode, brokerInfo) - failOnError(renderer.Render(tree, os.Stdout), "rendering failed", os.Exit) + metadataService := rabtap.NewInMemoryMetadataService(brokerInfo) + tree, _ := treeBuilder.BuildTree(cmd.rootNode, metadataService) + failOnError(renderer.Render(tree, cmd.out), "rendering failed", os.Exit) } diff --git a/cmd/rabtap/cmd_info_test.go b/cmd/rabtap/cmd_info_test.go index e1851e1..5d2801f 100644 --- a/cmd/rabtap/cmd_info_test.go +++ b/cmd/rabtap/cmd_info_test.go @@ -5,10 +5,10 @@ package main import ( + "bytes" "context" "crypto/tls" "net/url" - "os" "strings" "testing" @@ -30,13 +30,14 @@ func Example_startCmdInfo() { // http://rootnode/vhost (broker ver='3.6.9', mgmt ver='3.6.9', cluster='rabbit@08f57d1fe8ab') } -func Example_cmdInfoByExchangeInTextFormat() { +func TestCmdInfoByExchangeInTextFormatProducesExpectedTree(t *testing.T) { mock := testcommon.NewRabbitAPIMock(testcommon.MockModeStd) defer mock.Close() url, _ := url.Parse(mock.URL) client := rabtap.NewRabbitHTTPClient(url, &tls.Config{}) + var actual bytes.Buffer rootURL, _ := url.Parse("http://rabbitmq/api") cmdInfo(context.TODO(), CmdInfoArg{ @@ -46,44 +47,48 @@ func Example_cmdInfoByExchangeInTextFormat() { Mode: "byExchange", ShowConsumers: true, ShowDefaultExchange: false, - QueueFilter: TruePredicate, + Filter: TruePredicate, OmitEmptyExchanges: false}, renderConfig: BrokerInfoRendererConfig{ Format: "text", ShowStats: false, NoColor: true}, - out: os.Stdout}) - - // Output: - // http://rabbitmq/api (broker ver='3.6.9', mgmt ver='3.6.9', cluster='rabbit@08f57d1fe8ab') - // └── Vhost / - // ├── amq.direct (exchange, type 'direct', [D]) - // ├── amq.fanout (exchange, type 'fanout', [D]) - // ├── amq.headers (exchange, type 'headers', [D]) - // ├── amq.match (exchange, type 'headers', [D]) - // ├── amq.rabbitmq.log (exchange, type 'topic', [D|I]) - // ├── amq.rabbitmq.trace (exchange, type 'topic', [D|I]) - // ├── amq.topic (exchange, type 'topic', [D]) - // ├── test-direct (exchange, type 'direct', [D|AD|I]) - // │ ├── direct-q1 (queue(classic), key='direct-q1', running, [D]) - // │ │ ├── some_consumer (consumer user='guest', prefetch=0, chan='172.17.0.1:40874 -> 172.17.0.2:5672 (1)') - // │ │ │ └── '172.17.0.1:40874 -> 172.17.0.2:5672' (connection client='https://github.com/streadway/amqp', host='172.17.0.2:5672', peer='172.17.0.1:40874') - // │ │ └── another_consumer w/ faulty channel (consumer user='', prefetch=0, chan='') - // │ └── direct-q2 (queue(classic), key='direct-q2', running, [D]) - // ├── test-fanout (exchange, type 'fanout', [D]) - // │ ├── fanout-q1 (queue(classic), idle since 2017-05-25 19:14:32, [D]) - // │ └── fanout-q2 (queue(classic), idle since 2017-05-25 19:14:32, [D]) - // ├── test-headers (exchange, type 'headers', [D|AD]) - // │ ├── header-q1 (queue(classic), key='headers-q1', idle since 2017-05-25 19:14:53, [D]) - // │ └── header-q2 (queue(classic), key='headers-q2', idle since 2017-05-25 19:14:47, [D]) - // └── test-topic (exchange, type 'topic', [D]) - // ├── topic-q1 (queue(classic), key='topic-q1', idle since 2017-05-25 19:14:17, [D|AD|EX]) - // ├── topic-q2 (queue(classic), key='topic-q2', idle since 2017-05-25 19:14:21, [D]) - // └── test-topic (exchange, type 'topic', [D]) - + out: &actual}) + + expected := `http://rabbitmq/api (broker ver='3.6.9', mgmt ver='3.6.9', cluster='rabbit@08f57d1fe8ab') +└─ Vhost / + ├─ amq.direct (exchange(direct), [D]) + ├─ amq.fanout (exchange(fanout), [D]) + ├─ amq.headers (exchange(headers), [D]) + ├─ amq.match (exchange(headers), [D]) + ├─ amq.rabbitmq.log (exchange(topic), [D|I]) + ├─ amq.rabbitmq.trace (exchange(topic), [D|I]) + ├─ amq.topic (exchange(topic), [D]) + │ └─ test-topic (exchange(topic), key='test', [D]) + ├─ test-direct (exchange(direct), [D|AD|I]) + │ ├─ direct-q1 (queue(classic), key='direct-q1', running, [D]) + │ │ ├─ ? (connection ) + │ │ │ └─ ? (channel ) + │ │ │ └─ another_consumer w/ faulty channel (consumer prefetch=0, ack_req=no, active=no, status=) + │ │ └─ '172.17.0.1:40874 -> 172.17.0.2:5672' (connection guest@172.17.0.2:5672, state='running', client='https://github.com/streadway/amqp', ver='β', peer='172.17.0.1:40874') + │ │ └─ '172.17.0.1:40874 -> 172.17.0.2:5672 (1)' (channel prefetch=0, state=running, unacked=0, confirms=no) + │ │ └─ another_consumer w/ faulty channel (consumer prefetch=0, ack_req=no, active=no, status=) + │ └─ direct-q2 (queue(classic), key='direct-q2', running, [D]) + ├─ test-fanout (exchange(fanout), [D]) + │ ├─ fanout-q1 (queue(classic), idle since 2017-05-25 19:14:32, [D]) + │ └─ fanout-q2 (queue(classic), idle since 2017-05-25 19:14:32, [D]) + ├─ test-headers (exchange(headers), [D|AD]) + │ ├─ header-q1 (queue(classic), key='headers-q1', idle since 2017-05-25 19:14:53, [D]) + │ └─ header-q2 (queue(classic), key='headers-q2', idle since 2017-05-25 19:14:47, [D]) + └─ test-topic (exchange(topic), [D]) + ├─ topic-q1 (queue(classic), key='topic-q1', idle since 2017-05-25 19:14:17, [D|AD|EX]) + └─ topic-q2 (queue(classic), key='topic-q2', idle since 2017-05-25 19:14:21, [D]) +` + + assert.Equal(t, expected, actual.String()) } -func Example_cmdInfoByConnectionInTextFormat() { +func TestCmdInfoByConnectionInTextFormatProducesExpectedTree(t *testing.T) { mock := testcommon.NewRabbitAPIMock(testcommon.MockModeStd) defer mock.Close() @@ -91,6 +96,7 @@ func Example_cmdInfoByConnectionInTextFormat() { client := rabtap.NewRabbitHTTPClient(url, &tls.Config{}) rootURL, _ := url.Parse("http://rabbitmq/api") + var actual bytes.Buffer cmdInfo(context.TODO(), CmdInfoArg{ rootNode: rootURL, @@ -99,20 +105,23 @@ func Example_cmdInfoByConnectionInTextFormat() { Mode: "byConnection", ShowConsumers: true, ShowDefaultExchange: false, - QueueFilter: TruePredicate, + Filter: TruePredicate, OmitEmptyExchanges: false}, renderConfig: BrokerInfoRendererConfig{ Format: "text", ShowStats: false, NoColor: true}, - out: os.Stdout}) + out: &actual}) + + expected := `http://rabbitmq/api (broker ver='3.6.9', mgmt ver='3.6.9', cluster='rabbit@08f57d1fe8ab') +└─ Vhost / + └─ '172.17.0.1:40874 -> 172.17.0.2:5672' (connection guest@172.17.0.2:5672, state='running', client='https://github.com/streadway/amqp', ver='β', peer='172.17.0.1:40874') + └─ '172.17.0.1:40874 -> 172.17.0.2:5672 (1)' (channel prefetch=0, state=running, unacked=0, confirms=no) + └─ some_consumer (consumer prefetch=0, ack_req=no, active=no, status=) + └─ direct-q1 (queue(classic), running, [D]) +` + assert.Equal(t, expected, actual.String()) - // Output: - // http://rabbitmq/api (broker ver='3.6.9', mgmt ver='3.6.9', cluster='rabbit@08f57d1fe8ab') - // └── Vhost / - // └── '172.17.0.1:40874 -> 172.17.0.2:5672' (connection client='https://github.com/streadway/amqp', host='172.17.0.2:5672', peer='172.17.0.1:40874') - // └── some_consumer (consumer user='guest', prefetch=0, chan='172.17.0.1:40874 -> 172.17.0.2:5672 (1)') - // └── direct-q1 (queue(classic), running, [D]) } const expectedResultDotByExchange = `graph broker { @@ -121,85 +130,85 @@ const expectedResultDotByExchange = `graph broker { "root" -- "vhost_/"; "vhost_/" [shape="box", label="Virtual host /"]; -"vhost_/" -- "exchange_amq.direct"[headport=n]; -"vhost_/" -- "exchange_amq.fanout"[headport=n]; -"vhost_/" -- "exchange_amq.headers"[headport=n]; -"vhost_/" -- "exchange_amq.match"[headport=n]; -"vhost_/" -- "exchange_amq.rabbitmq.log"[headport=n]; -"vhost_/" -- "exchange_amq.rabbitmq.trace"[headport=n]; -"vhost_/" -- "exchange_amq.topic"[headport=n]; -"vhost_/" -- "exchange_test-direct"[headport=n]; -"vhost_/" -- "exchange_test-fanout"[headport=n]; -"vhost_/" -- "exchange_test-headers"[headport=n]; -"vhost_/" -- "exchange_test-topic"[headport=n]; +"vhost_/" -- "exchange_/_amq.direct"[headport=n]; +"vhost_/" -- "exchange_/_amq.fanout"[headport=n]; +"vhost_/" -- "exchange_/_amq.headers"[headport=n]; +"vhost_/" -- "exchange_/_amq.match"[headport=n]; +"vhost_/" -- "exchange_/_amq.rabbitmq.log"[headport=n]; +"vhost_/" -- "exchange_/_amq.rabbitmq.trace"[headport=n]; +"vhost_/" -- "exchange_/_amq.topic"[headport=n]; +"vhost_/" -- "exchange_/_test-direct"[headport=n]; +"vhost_/" -- "exchange_/_test-fanout"[headport=n]; +"vhost_/" -- "exchange_/_test-headers"[headport=n]; +"vhost_/" -- "exchange_/_test-topic"[headport=n]; -"exchange_amq.direct" [shape="record"; label="{ amq.direct |direct | { D | | } }"]; +"exchange_/_amq.direct" [shape="record"; label="{ amq.direct |direct | { D | | } }"]; -"exchange_amq.fanout" [shape="record"; label="{ amq.fanout |fanout | { D | | } }"]; +"exchange_/_amq.fanout" [shape="record"; label="{ amq.fanout |fanout | { D | | } }"]; -"exchange_amq.headers" [shape="record"; label="{ amq.headers |headers | { D | | } }"]; +"exchange_/_amq.headers" [shape="record"; label="{ amq.headers |headers | { D | | } }"]; -"exchange_amq.match" [shape="record"; label="{ amq.match |headers | { D | | } }"]; +"exchange_/_amq.match" [shape="record"; label="{ amq.match |headers | { D | | } }"]; -"exchange_amq.rabbitmq.log" [shape="record"; label="{ amq.rabbitmq.log |topic | { D | | I } }"]; +"exchange_/_amq.rabbitmq.log" [shape="record"; label="{ amq.rabbitmq.log |topic | { D | | I } }"]; -"exchange_amq.rabbitmq.trace" [shape="record"; label="{ amq.rabbitmq.trace |topic | { D | | I } }"]; +"exchange_/_amq.rabbitmq.trace" [shape="record"; label="{ amq.rabbitmq.trace |topic | { D | | I } }"]; -"exchange_amq.topic" [shape="record"; label="{ amq.topic |topic | { D | | } }"]; +"exchange_/_amq.topic" [shape="record"; label="{ amq.topic |topic | { D | | } }"]; +"exchange_/_amq.topic" -- "exchange_/_test-topic" [fontsize=10; headport=n; label="test"]; -"exchange_test-direct" [shape="record"; label="{ test-direct |direct | { D | AD | I } }"]; +"exchange_/_test-topic" [shape="record"; label="{ test-topic |topic | { D | | } }"]; -"exchange_test-direct" -- "boundqueue_direct-q1" [fontsize=10; headport=n; label="direct-q1"]; -"exchange_test-direct" -- "boundqueue_direct-q2" [fontsize=10; headport=n; label="direct-q2"]; -"boundqueue_direct-q1" [shape="record"; label="{ direct-q1 | { D | | } }"]; +"exchange_/_test-direct" [shape="record"; label="{ test-direct |direct | { D | AD | I } }"]; +"exchange_/_test-direct" -- "queue_/_direct-q1" [fontsize=10; headport=n; label="direct-q1"]; +"exchange_/_test-direct" -- "queue_/_direct-q2" [fontsize=10; headport=n; label="direct-q2"]; -"boundqueue_direct-q2" [shape="record"; label="{ direct-q2 | { D | | } }"]; +"queue_/_direct-q1" [shape="record"; label="{ direct-q1 | { D | | } }"]; -"exchange_test-fanout" [shape="record"; label="{ test-fanout |fanout | { D | | } }"]; +"queue_/_direct-q2" [shape="record"; label="{ direct-q2 | { D | | } }"]; -"exchange_test-fanout" -- "boundqueue_fanout-q1" [fontsize=10; headport=n; label=""]; -"exchange_test-fanout" -- "boundqueue_fanout-q2" [fontsize=10; headport=n; label=""]; -"boundqueue_fanout-q1" [shape="record"; label="{ fanout-q1 | { D | | } }"]; +"exchange_/_test-fanout" [shape="record"; label="{ test-fanout |fanout | { D | | } }"]; +"exchange_/_test-fanout" -- "queue_/_fanout-q1" [fontsize=10; headport=n; label=""]; +"exchange_/_test-fanout" -- "queue_/_fanout-q2" [fontsize=10; headport=n; label=""]; -"boundqueue_fanout-q2" [shape="record"; label="{ fanout-q2 | { D | | } }"]; +"queue_/_fanout-q1" [shape="record"; label="{ fanout-q1 | { D | | } }"]; -"exchange_test-headers" [shape="record"; label="{ test-headers |headers | { D | AD | } }"]; +"queue_/_fanout-q2" [shape="record"; label="{ fanout-q2 | { D | | } }"]; -"exchange_test-headers" -- "boundqueue_header-q1" [fontsize=10; headport=n; label="headers-q1"]; -"exchange_test-headers" -- "boundqueue_header-q2" [fontsize=10; headport=n; label="headers-q2"]; -"boundqueue_header-q1" [shape="record"; label="{ header-q1 | { D | | } }"]; +"exchange_/_test-headers" [shape="record"; label="{ test-headers |headers | { D | AD | } }"]; +"exchange_/_test-headers" -- "queue_/_header-q1" [fontsize=10; headport=n; label="headers-q1"]; +"exchange_/_test-headers" -- "queue_/_header-q2" [fontsize=10; headport=n; label="headers-q2"]; -"boundqueue_header-q2" [shape="record"; label="{ header-q2 | { D | | } }"]; +"queue_/_header-q1" [shape="record"; label="{ header-q1 | { D | | } }"]; -"exchange_test-topic" [shape="record"; label="{ test-topic |topic | { D | | } }"]; +"queue_/_header-q2" [shape="record"; label="{ header-q2 | { D | | } }"]; -"exchange_test-topic" -- "boundqueue_topic-q1" [fontsize=10; headport=n; label="topic-q1"]; -"exchange_test-topic" -- "boundqueue_topic-q2" [fontsize=10; headport=n; label="topic-q2"]; -"exchange_test-topic" -- "exchange_test-topic" [fontsize=10; headport=n; label=""]; -"boundqueue_topic-q1" [shape="record"; label="{ topic-q1 | { D | AD | EX } }"]; +"exchange_/_test-topic" [shape="record"; label="{ test-topic |topic | { D | | } }"]; +"exchange_/_test-topic" -- "queue_/_topic-q1" [fontsize=10; headport=n; label="topic-q1"]; +"exchange_/_test-topic" -- "queue_/_topic-q2" [fontsize=10; headport=n; label="topic-q2"]; -"boundqueue_topic-q2" [shape="record"; label="{ topic-q2 | { D | | } }"]; +"queue_/_topic-q1" [shape="record"; label="{ topic-q1 | { D | AD | EX } }"]; -"exchange_test-topic" [shape="record"; label="{ test-topic |topic | { D | | } }"]; +"queue_/_topic-q2" [shape="record"; label="{ topic-q2 | { D | | } }"]; }` @@ -211,27 +220,49 @@ func TestCmdInfoByExchangeInDotFormat(t *testing.T) { client := rabtap.NewRabbitHTTPClient(url, &tls.Config{}) rootURL, _ := url.Parse("http://rabbitmq/api") - testfunc := func() { - cmdInfo( - context.TODO(), - CmdInfoArg{ - rootNode: rootURL, - client: client, - treeConfig: BrokerInfoTreeBuilderConfig{ - Mode: "byExchange", - ShowConsumers: false, - ShowDefaultExchange: false, - QueueFilter: TruePredicate, - OmitEmptyExchanges: false}, - renderConfig: BrokerInfoRendererConfig{Format: "dot"}, - out: os.Stdout}) - } - result := testcommon.CaptureOutput(testfunc) + var actual bytes.Buffer + cmdInfo( + context.TODO(), + CmdInfoArg{ + rootNode: rootURL, + client: client, + treeConfig: BrokerInfoTreeBuilderConfig{ + Mode: "byExchange", + ShowConsumers: false, + ShowDefaultExchange: false, + Filter: TruePredicate, + OmitEmptyExchanges: false}, + renderConfig: BrokerInfoRendererConfig{Format: "dot"}, + out: &actual}) + assert.Equal(t, strings.Trim(expectedResultDotByExchange, " \n"), - strings.Trim(result, " \n")) + strings.Trim(actual.String(), " \n")) } -const expectedResultDotByConnection = `graph broker { +func TestCmdInfoByConnectionInDotFormat(t *testing.T) { + + mock := testcommon.NewRabbitAPIMock(testcommon.MockModeStd) + defer mock.Close() + url, _ := url.Parse(mock.URL) + client := rabtap.NewRabbitHTTPClient(url, &tls.Config{}) + rootURL, _ := url.Parse("http://rabbitmq/api") + + var actual bytes.Buffer + cmdInfo( + context.TODO(), + CmdInfoArg{ + rootNode: rootURL, + client: client, + treeConfig: BrokerInfoTreeBuilderConfig{ + Mode: "byConnection", + ShowConsumers: false, + ShowDefaultExchange: false, + Filter: TruePredicate, + OmitEmptyExchanges: false}, + renderConfig: BrokerInfoRendererConfig{Format: "dot"}, + out: &actual}) + + const expected = `graph broker { "root" [shape="record", label="{RabbitMQ 3.6.9 |http://rabbitmq/api |rabbit@08f57d1fe8ab }"]; "root" -- "vhost_/"; @@ -239,40 +270,18 @@ const expectedResultDotByConnection = `graph broker { "vhost_/" -- "connection_172.17.0.1:40874 -> 172.17.0.2:5672"[headport=n]; -"connection_172.17.0.1:40874 -> 172.17.0.2:5672" [shape="record" label="172.17.0.1:40874 -> 172.17.0.2:5672"]; +"connection_172.17.0.1:40874 -> 172.17.0.2:5672" [shape="record" label="172.17.0.1:40874 -> 172.17.0.2:5672"]; + +"connection_172.17.0.1:40874 -> 172.17.0.2:5672" -- "channel_172.17.0.1:40874 -> 172.17.0.2:5672 (1)" +"channel_172.17.0.1:40874 -> 172.17.0.2:5672 (1)" [shape="record" label="172.17.0.1:40874 -> 172.17.0.2:5672 (1)"]; -"connection_172.17.0.1:40874 -> 172.17.0.2:5672" -- "consumer_some_consumer" +"channel_172.17.0.1:40874 -> 172.17.0.2:5672 (1)" -- "consumer_some_consumer" "consumer_some_consumer" [shape="record" label="some_consumer"]; -"consumer_some_consumer" -- "queue_direct-q1" -"queue_direct-q1" [shape="record"; label="{ direct-q1 | { D | | } }"]; +"consumer_some_consumer" -- "queue_/_direct-q1" +"queue_/_direct-q1" [shape="record"; label="{ direct-q1 | { D | | } }"]; }` - -func TestCmdInfoByConnectionInDotFormat(t *testing.T) { - - mock := testcommon.NewRabbitAPIMock(testcommon.MockModeStd) - defer mock.Close() - url, _ := url.Parse(mock.URL) - client := rabtap.NewRabbitHTTPClient(url, &tls.Config{}) - rootURL, _ := url.Parse("http://rabbitmq/api") - - testfunc := func() { - cmdInfo( - context.TODO(), - CmdInfoArg{ - rootNode: rootURL, - client: client, - treeConfig: BrokerInfoTreeBuilderConfig{ - Mode: "byConnection", - ShowConsumers: false, - ShowDefaultExchange: false, - QueueFilter: TruePredicate, - OmitEmptyExchanges: false}, - renderConfig: BrokerInfoRendererConfig{Format: "dot"}, - out: os.Stdout}) - } - result := testcommon.CaptureOutput(testfunc) - assert.Equal(t, strings.Trim(expectedResultDotByConnection, " \n"), - strings.Trim(result, " \n")) + assert.Equal(t, strings.Trim(expected, " \n"), + strings.Trim(actual.String(), " \n")) } diff --git a/cmd/rabtap/cmd_queue_test.go b/cmd/rabtap/cmd_queue_test.go index 36f708b..025512c 100644 --- a/cmd/rabtap/cmd_queue_test.go +++ b/cmd/rabtap/cmd_queue_test.go @@ -1,5 +1,6 @@ // Copyright (C) 2017 Jan Delgado +//go:build integration // +build integration package main @@ -7,6 +8,7 @@ package main import ( "context" "crypto/tls" + "fmt" "net/url" "os" "testing" @@ -24,6 +26,20 @@ func TestAmqpHeaderRoutingModeConverts(t *testing.T) { assert.Equal(t, "", amqpHeaderRoutingMode(HeaderNone)) } +func findQueueByName(apiURL *url.URL, vhost, name string) (*rabtap.RabbitQueue, error) { + client := rabtap.NewRabbitHTTPClient(apiURL, &tls.Config{}) + queues, err := client.Queues(context.TODO()) + if err != nil { + return nil, err + } + for _, q := range queues { + if q.Name == name && q.Vhost == vhost { + return &q, nil + } + } + return nil, fmt.Errorf("queue not found") +} + func TestIntegrationCmdQueueCreatePurgeiBindUnbindQueue(t *testing.T) { // integration tests queue creation, bind to exchange, purge, @@ -55,15 +71,11 @@ func TestIntegrationCmdQueueCreatePurgeiBindUnbindQueue(t *testing.T) { time.Sleep(2 * time.Second) - // TODO add a simple client to testcommon - client := rabtap.NewRabbitHTTPClient(apiURL, &tls.Config{}) - queues, err := client.Queues(context.TODO()) - assert.Nil(t, err) - i := rabtap.FindQueueByName(queues, "/", testQueue) - require.True(t, i != -1) + // Check that the queue was created using the REST API + queue, err := findQueueByName(apiURL, "/", testQueue) + require.NoError(t, err) // check that queue is empty - queue := queues[i] assert.Equal(t, 0, queue.Messages) // unbind queue diff --git a/cmd/rabtap/cmd_tap_test.go b/cmd/rabtap/cmd_tap_test.go index 84b4a34..498aad5 100644 --- a/cmd/rabtap/cmd_tap_test.go +++ b/cmd/rabtap/cmd_tap_test.go @@ -1,5 +1,6 @@ // Copyright (C) 2017 Jan Delgado +//go:build integration // +build integration package main diff --git a/cmd/rabtap/color_printer.go b/cmd/rabtap/color_printer.go index 750072d..be72aea 100644 --- a/cmd/rabtap/color_printer.go +++ b/cmd/rabtap/color_printer.go @@ -19,12 +19,14 @@ const ( colorExchange = color.FgHiBlue colorQueue = color.FgHiYellow colorConnection = color.FgRed - colorChannel = color.FgWhite + colorChannel = color.FgHiMagenta colorConsumer = color.FgHiGreen colorMessage = color.FgHiYellow colorKey = color.FgHiCyan ) +var colorError = color.New(color.FgHiRed, color.BgWhite) + // ColorPrinterFunc takes fmt.Sprint like arguments and add colors type ColorPrinterFunc func(a ...interface{}) string @@ -39,6 +41,7 @@ type ColorPrinter struct { Consumer ColorPrinterFunc Message ColorPrinterFunc Key ColorPrinterFunc + Error ColorPrinterFunc } // GetFuncMap returns a function map that can be used in a template. @@ -52,7 +55,8 @@ func (s ColorPrinter) GetFuncMap() template.FuncMap { "VHostColor": s.VHost, "ConsumerColor": s.Consumer, "MessageColor": s.Message, - "KeyColor": s.Key} + "KeyColor": s.Key, + "ErrorColor": s.Error} } // NewColorableWriter returns a colorable writer for the given file (e.g @@ -74,6 +78,7 @@ func NewColorPrinter(noColor bool) ColorPrinter { nullPrinter, nullPrinter, nullPrinter, + nullPrinter, nullPrinter} } return ColorPrinter{ @@ -86,5 +91,6 @@ func NewColorPrinter(noColor bool) ColorPrinter { Consumer: color.New(colorConsumer).SprintFunc(), Message: color.New(colorMessage).SprintFunc(), Key: color.New(colorKey).SprintFunc(), + Error: colorError.SprintFunc(), } } diff --git a/cmd/rabtap/main.go b/cmd/rabtap/main.go index 3832403..77dd247 100644 --- a/cmd/rabtap/main.go +++ b/cmd/rabtap/main.go @@ -83,7 +83,7 @@ func startCmdInfo(ctx context.Context, args CommandLineArgs, titleURL *url.URL) Mode: args.InfoMode, ShowConsumers: args.ShowConsumers, ShowDefaultExchange: args.ShowDefaultExchange, - QueueFilter: queueFilter, + Filter: queueFilter, OmitEmptyExchanges: args.OmitEmptyExchanges}, renderConfig: BrokerInfoRendererConfig{ Format: args.Format, diff --git a/cmd/rabtap/rabtap_template_funcs.go b/cmd/rabtap/rabtap_template_funcs.go index 9883687..be17629 100644 --- a/cmd/rabtap/rabtap_template_funcs.go +++ b/cmd/rabtap/rabtap_template_funcs.go @@ -17,8 +17,17 @@ func (s rabtapTemplateFuncs) toPercent(x float64) int { return int(math.Round(x * 100.)) } +// asYesNo converts the given bool to "yes" or "no" +func (s rabtapTemplateFuncs) asYesNo(b bool) string { + if b { + return "yes" + } + return "no" +} + func (s rabtapTemplateFuncs) GetFuncMap() template.FuncMap { return template.FuncMap{ "ToPercent": s.toPercent, + "YesNo": s.asYesNo, } } diff --git a/cmd/rabtap/rabtap_template_funcs_test.go b/cmd/rabtap/rabtap_template_funcs_test.go index 742d2cf..3fb12cc 100644 --- a/cmd/rabtap/rabtap_template_funcs_test.go +++ b/cmd/rabtap/rabtap_template_funcs_test.go @@ -14,3 +14,10 @@ func TestFloatsAreConvertedIntoPercentageValues(t *testing.T) { assert.Equal(t, 100, f.toPercent(0.999)) assert.Equal(t, 100, f.toPercent(1.0)) } + +func TestBoolIsConvertedToYesOrNo(t *testing.T) { + f := rabtapTemplateFuncs{} + + assert.Equal(t, "no", f.asYesNo(false)) + assert.Equal(t, "yes", f.asYesNo(true)) +} diff --git a/cmd/rabtap/tree.go b/cmd/rabtap/tree.go index 0992718..dbea215 100644 --- a/cmd/rabtap/tree.go +++ b/cmd/rabtap/tree.go @@ -61,9 +61,9 @@ func PrintTree(node *TreeNode, buffer io.Writer) { case parent.Parent == nil: // nop case parent.IsLastChild(): - treeLines = " " + treeLines + treeLines = " " + treeLines default: - treeLines = "│ " + treeLines + treeLines = "│ " + treeLines } } @@ -71,9 +71,9 @@ func PrintTree(node *TreeNode, buffer io.Writer) { case node.Parent == nil: // no treeLine for root element case node.IsLastChild(): - treeLines += "└── " + treeLines += "└─ " default: - treeLines += "├── " + treeLines += "├─ " } fmt.Fprintf(buffer, "%s%s\n", treeLines, node.Text) diff --git a/cmd/rabtap/tree_test.go b/cmd/rabtap/tree_test.go index 37afb90..938a12e 100644 --- a/cmd/rabtap/tree_test.go +++ b/cmd/rabtap/tree_test.go @@ -61,9 +61,9 @@ func ExamplePrintTree() { // Output: // root - // ├── child1 - // │ └── child1.1 - // ├── child2 - // └── child3 - // └── child4 + // ├─ child1 + // │ └─ child1.1 + // ├─ child2 + // └─ child3 + // └─ child4 } diff --git a/cmd/testgen/testgen.go b/cmd/testgen/testgen.go index df233c5..cc63a75 100644 --- a/cmd/testgen/testgen.go +++ b/cmd/testgen/testgen.go @@ -12,6 +12,7 @@ package main // amqp://guest:guest@127.0.0.1:5672 import ( + "context" "flag" "fmt" "log" @@ -128,7 +129,8 @@ func generateTestMessages(ch *amqp.Channel, exchanges []string, numTestQueues in routingKey, headers := getRoutingKeyForExchange(exchange, i) log.Printf("publishing msg #%d to exchange '%s' with routing key '%s' and headers %#+v", count, exchange, routingKey, headers) - err := ch.Publish( + err := ch.PublishWithContext( + context.TODO(), exchange, routingKey, false, // mandatory diff --git a/go.mod b/go.mod index 54d45eb..da3e36f 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,23 @@ go 1.18 require ( github.com/Knetic/govaluate v0.0.0-20171022003610-9aa49832a739 //v0.0.0-20171022003610-9aa49832a739 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 - github.com/fatih/color v1.7.0 - github.com/google/uuid v1.1.1 - github.com/mattn/go-colorable v0.1.1 - github.com/rabbitmq/amqp091-go v1.3.4 - github.com/sirupsen/logrus v1.3.0 - github.com/stretchr/testify v1.3.0 - golang.org/x/net v0.0.0-20190620200207-3b0461eec859 - golang.org/x/sync v0.0.0-20190423024810-112230192c58 + github.com/fatih/color v1.13.0 + github.com/google/uuid v1.3.0 + github.com/mattn/go-colorable v0.1.13 + github.com/rabbitmq/amqp091-go v1.5.0 + github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.8.0 + golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 + golang.org/x/sync v0.0.0-20220907140024-f12130a52804 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect - github.com/mattn/go-isatty v0.0.6 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect + golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 43a6106..9dffc15 100644 --- a/go.sum +++ b/go.sum @@ -7,42 +7,102 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rabbitmq/amqp091-go v1.3.4 h1:tXuIslN1nhDqs2t6Jrz3BAoqvt4qIZzxvdbdcxWtHYU= github.com/rabbitmq/amqp091-go v1.3.4/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= +github.com/rabbitmq/amqp091-go v1.5.0 h1:VouyHPBu1CrKyJVfteGknGOGCzmOz0zcv/tONLkb7rg= +github.com/rabbitmq/amqp091-go v1.5.0/go.mod h1:JsV0ofX5f1nwOGafb8L5rBItt9GyhfQfcJj+oyz0dGg= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 h1:1WGATo9HAhkWMbfyuVU0tEFP88OIkUvwaHFveQPvzCQ= +golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A= +golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/publish.go b/pkg/publish.go index 1e602a4..c583025 100644 --- a/pkg/publish.go +++ b/pkg/publish.go @@ -187,7 +187,8 @@ func (s *AmqpPublish) createWorkerFunc( s.logger.Debugf("publish message to %s (%d bytes)", message.Routing, size) headers := EnsureAMQPTable(message.Routing.Headers()).(amqp.Table) message.Publishing.Headers = headers - err := session.Publish( + err := session.PublishWithContext( + ctx, message.Routing.Exchange(), message.Routing.Key(), s.mandatory, diff --git a/pkg/rabbitmq_api_model.go b/pkg/rabbitmq_api_model.go new file mode 100644 index 0000000..923e2f1 --- /dev/null +++ b/pkg/rabbitmq_api_model.go @@ -0,0 +1,444 @@ +// rabitmq api model contains all data structures used by the rabbitmq HTTP +// API. To make things simple, these structures are also used as the our +// domain model. +// Copyright (C) 2017-2022 Jan Delgado + +package rabtap + +// RabbitVhost models the /vhosts resource of the rabbitmq http api +type RabbitVhost struct { + // ClusterState struct { + // Rabbit1A92B8526E33 string `json:"rabbit@1a92b8526e33"` + // } `json:"cluster_state"` + Description string `json:"description"` + MessageStats struct { + Ack int `json:"ack"` + AckDetails struct { + Rate float64 `json:"rate"` + } `json:"ack_details"` + Confirm int `json:"confirm"` + ConfirmDetails struct { + Rate float64 `json:"rate"` + } `json:"confirm_details"` + Deliver int `json:"deliver"` + DeliverDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_details"` + DeliverGet int `json:"deliver_get"` + DeliverGetDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_get_details"` + DeliverNoAck int `json:"deliver_no_ack"` + DeliverNoAckDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_no_ack_details"` + DropUnroutable int `json:"drop_unroutable"` + DropUnroutableDetails struct { + Rate float64 `json:"rate"` + } `json:"drop_unroutable_details"` + Get int `json:"get"` + GetDetails struct { + Rate float64 `json:"rate"` + } `json:"get_details"` + GetEmpty int `json:"get_empty"` + GetEmptyDetails struct { + Rate float64 `json:"rate"` + } `json:"get_empty_details"` + GetNoAck int `json:"get_no_ack"` + GetNoAckDetails struct { + Rate float64 `json:"rate"` + } `json:"get_no_ack_details"` + Publish int `json:"publish"` + PublishDetails struct { + Rate float64 `json:"rate"` + } `json:"publish_details"` + Redeliver int `json:"redeliver"` + RedeliverDetails struct { + Rate float64 `json:"rate"` + } `json:"redeliver_details"` + ReturnUnroutable int `json:"return_unroutable"` + ReturnUnroutableDetails struct { + Rate float64 `json:"rate"` + } `json:"return_unroutable_details"` + } `json:"message_stats"` + Messages int `json:"messages"` + MessagesDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_details"` + MessagesReady int `json:"messages_ready"` + MessagesReadyDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_ready_details"` + MessagesUnacknowledged int `json:"messages_unacknowledged"` + MessagesUnacknowledgedDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_unacknowledged_details"` + Metadata struct { + Description string `json:"description"` + Tags []interface{} `json:"tags"` + } `json:"metadata"` + Name string `json:"name"` + RecvOct int `json:"recv_oct"` + RecvOctDetails struct { + Rate float64 `json:"rate"` + } `json:"recv_oct_details"` + SendOct int `json:"send_oct"` + SendOctDetails struct { + Rate float64 `json:"rate"` + } `json:"send_oct_details"` + Tags []interface{} `json:"tags"` + Tracing bool `json:"tracing"` +} + +// RabbitConnection models the /connections resource of the rabbitmq http api +type RabbitConnection struct { + ReductionsDetails struct { + Rate float64 `json:"rate"` + } `json:"reductions_details"` + Reductions int `json:"reductions"` + RecvOctDetails struct { + Rate float64 `json:"rate"` + } `json:"recv_oct_details"` + RecvOct int `json:"recv_oct"` + SendOctDetails struct { + Rate float64 `json:"rate"` + } `json:"send_oct_details"` + SendOct int `json:"send_oct"` + ConnectedAt int64 `json:"connected_at"` + ClientProperties struct { + Product string `json:"product"` + Version string `json:"version"` + ConnectionName string `json:"connection_name"` + Capabilities struct { + ConnectionBlocked bool `json:"connection.blocked"` + ConsumerCancelNotify bool `json:"consumer_cancel_notify"` + } `json:"capabilities"` + } `json:"client_properties"` + ChannelMax int `json:"channel_max"` + FrameMax int `json:"frame_max"` + Timeout int `json:"timeout"` + Vhost string `json:"vhost"` + User string `json:"user"` + Protocol string `json:"protocol"` + SslHash interface{} `json:"ssl_hash"` + SslCipher interface{} `json:"ssl_cipher"` + SslKeyExchange interface{} `json:"ssl_key_exchange"` + SslProtocol interface{} `json:"ssl_protocol"` + AuthMechanism string `json:"auth_mechanism"` + PeerCertValidity interface{} `json:"peer_cert_validity"` + PeerCertIssuer interface{} `json:"peer_cert_issuer"` + PeerCertSubject interface{} `json:"peer_cert_subject"` + Ssl bool `json:"ssl"` + PeerHost string `json:"peer_host"` + Host string `json:"host"` + PeerPort int `json:"peer_port"` + Port int `json:"port"` + Name string `json:"name"` + Node string `json:"node"` + Type string `json:"type"` + GarbageCollection struct { + MinorGcs int `json:"minor_gcs"` + FullsweepAfter int `json:"fullsweep_after"` + MinHeapSize int `json:"min_heap_size"` + MinBinVheapSize int `json:"min_bin_vheap_size"` + MaxHeapSize int `json:"max_heap_size"` + } `json:"garbage_collection"` + Channels int `json:"channels"` + State string `json:"state"` + SendPend int `json:"send_pend"` + SendCnt int `json:"send_cnt"` + RecvCnt int `json:"recv_cnt"` +} + +// RabbitChannel models the /channels resource of the rabbitmq http api +type RabbitChannel struct { + ReductionsDetails struct { + Rate float64 `json:"rate"` + } `json:"reductions_details"` + Reductions int `json:"reductions"` + MessageStats struct { + ReturnUnroutableDetails struct { + Rate float64 `json:"rate"` + } `json:"return_unroutable_details"` + ReturnUnroutable int `json:"return_unroutable"` + ConfirmDetails struct { + Rate float64 `json:"rate"` + } `json:"confirm_details"` + Confirm int `json:"confirm"` + PublishDetails struct { + Rate float64 `json:"rate"` + } `json:"publish_details"` + Publish int `json:"publish"` + Ack int `json:"ack"` + AckDetails struct { + Rate float64 `json:"rate"` + } `json:"ack_details"` + Deliver int `json:"deliver"` + DeliverDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_details"` + DeliverGet int `json:"deliver_get"` + DeliverGetDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_get_details"` + DeliverNoAck int `json:"deliver_no_ack"` + DeliverNoAckDetails struct { + Rate float64 `json:"rate"` + } `json:"deliver_no_ack_details"` + Get int `json:"get"` + GetDetails struct { + Rate float64 `json:"rate"` + } `json:"get_details"` + GetEmpty int `json:"get_empty"` + GetEmptyDetails struct { + Rate float64 `json:"rate"` + } `json:"get_empty_details"` + GetNoAck int `json:"get_no_ack"` + GetNoAckDetails struct { + Rate float64 `json:"rate"` + } `json:"get_no_ack_details"` + Redeliver int `json:"redeliver"` + RedeliverDetails struct { + Rate float64 `json:"rate"` + } `json:"redeliver_details"` + } `json:"message_stats"` + Vhost string `json:"vhost"` + User string `json:"user"` + Number int `json:"number"` + Name string `json:"name"` + Node string `json:"node"` + ConnectionDetails ConnectionDetails `json:"connection_details"` + GarbageCollection struct { + MinorGcs int `json:"minor_gcs"` + FullsweepAfter int `json:"fullsweep_after"` + MinHeapSize int `json:"min_heap_size"` + MinBinVheapSize int `json:"min_bin_vheap_size"` + MaxHeapSize int `json:"max_heap_size"` + } `json:"garbage_collection"` + State string `json:"state"` + GlobalPrefetchCount int `json:"global_prefetch_count"` + PrefetchCount int `json:"prefetch_count"` + AcksUncommitted int `json:"acks_uncommitted"` + MessagesUncommitted int `json:"messages_uncommitted"` + MessagesUnconfirmed int `json:"messages_unconfirmed"` + MessagesUnacknowledged int `json:"messages_unacknowledged"` + ConsumerCount int `json:"consumer_count"` + Confirm bool `json:"confirm"` + Transactional bool `json:"transactional"` + IdleSince string `json:"idle_since"` +} + +// RabbitOverview models the /overview resource of the rabbitmq http api +type RabbitOverview struct { + ManagementVersion string `json:"management_version"` + RatesMode string `json:"rates_mode"` + ExchangeTypes []struct { + Name string `json:"name"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + } `json:"exchange_types"` + RabbitmqVersion string `json:"rabbitmq_version"` + ClusterName string `json:"cluster_name"` + ErlangVersion string `json:"erlang_version"` + ErlangFullVersion string `json:"erlang_full_version"` + MessageStats struct { + DiskReads int `json:"disk_reads"` + DiskReadsDetails struct { + Rate float64 `json:"rate"` + } `json:"disk_reads_details"` + DiskWrites int `json:"disk_writes"` + DiskWritesDetails struct { + Rate float64 `json:"rate"` + } `json:"disk_writes_details"` + } `json:"message_stats"` + QueueTotals struct { + MessagesReady int `json:"messages_ready"` + MessagesReadyDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_ready_details"` + MessagesUnacknowledged int `json:"messages_unacknowledged"` + MessagesUnacknowledgedDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_unacknowledged_details"` + Messages int `json:"messages"` + MessagesDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_details"` + } `json:"queue_totals"` + ObjectTotals struct { + Consumers int `json:"consumers"` + Queues int `json:"queues"` + Exchanges int `json:"exchanges"` + Connections int `json:"connections"` + Channels int `json:"channels"` + } `json:"object_totals"` + StatisticsDbEventQueue int `json:"statistics_db_event_queue"` + Node string `json:"node"` + Listeners []struct { + Node string `json:"node"` + Protocol string `json:"protocol"` + IPAddress string `json:"ip_address"` + Port int `json:"port"` + // workaround for rabbitmq returning empty array OR valid object + // here. TODO check / further investigate.- + /*Dummy []interface{} `json:"socket_opts,omitempty"` + SocketOpts struct { + Backlog int `json:"backlog"` + Nodelay bool `json:"nodelay"` + //Linger []interface{} `json:"linger"` + ExitOnClose bool `json:"exit_on_close"` + } `json:"socket_opts"`*/ + } `json:"listeners"` + Contexts []struct { + Node string `json:"node"` + Description string `json:"description"` + Path string `json:"path"` + Port string `json:"port"` + Ssl string `json:"ssl"` + } `json:"contexts"` +} + +// RabbitQueue models the /queues resource of the rabbitmq http api +type RabbitQueue struct { + MessagesDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_details"` + Messages int `json:"messages"` + MessagesUnacknowledgedDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_unacknowledged_details"` + MessagesUnacknowledged int `json:"messages_unacknowledged"` + MessagesReadyDetails struct { + Rate float64 `json:"rate"` + } `json:"messages_ready_details"` + MessagesReady int `json:"messages_ready"` + ReductionsDetails struct { + Rate float64 `json:"rate"` + } `json:"reductions_details"` + Reductions int `json:"reductions"` + Node string `json:"node"` + Arguments struct { + } `json:"arguments"` + Exclusive bool `json:"exclusive"` + AutoDelete bool `json:"auto_delete"` + Durable bool `json:"durable"` + Vhost string `json:"vhost"` + Name string `json:"name"` + Type string `json:"type"` + MessageBytesPagedOut int `json:"message_bytes_paged_out"` + MessagesPagedOut int `json:"messages_paged_out"` + BackingQueueStatus struct { + Mode string `json:"mode"` + Q1 int `json:"q1"` + Q2 int `json:"q2"` + // Delta []interface{} `json:"delta"` + Q3 int `json:"q3"` + Q4 int `json:"q4"` + Len int `json:"len"` + // TargetRAMCount int `json:"target_ram_count"` // string or int -> need further research here when attr is in need ("infinity") + NextSeqID int `json:"next_seq_id"` + AvgIngressRate float64 `json:"avg_ingress_rate"` + AvgEgressRate float64 `json:"avg_egress_rate"` + AvgAckIngressRate float64 `json:"avg_ack_ingress_rate"` + AvgAckEgressRate float64 `json:"avg_ack_egress_rate"` + } `json:"backing_queue_status"` + // HeadMessageTimestamp interface{} `json:"head_message_timestamp"` + MessageBytesPersistent int `json:"message_bytes_persistent"` + MessageBytesRAM int `json:"message_bytes_ram"` + MessageBytesUnacknowledged int `json:"message_bytes_unacknowledged"` + MessageBytesReady int `json:"message_bytes_ready"` + MessageBytes int `json:"message_bytes"` + MessagesPersistent int `json:"messages_persistent"` + MessagesUnacknowledgedRAM int `json:"messages_unacknowledged_ram"` + MessagesReadyRAM int `json:"messages_ready_ram"` + MessagesRAM int `json:"messages_ram"` + GarbageCollection struct { + MinorGcs int `json:"minor_gcs"` + FullsweepAfter int `json:"fullsweep_after"` + MinHeapSize int `json:"min_heap_size"` + MinBinVheapSize int `json:"min_bin_vheap_size"` + MaxHeapSize int `json:"max_heap_size"` + } `json:"garbage_collection"` + State string `json:"state"` + // RecoverableSlaves interface{} `json:"recoverable_slaves"` + Consumers int `json:"consumers"` + // ExclusiveConsumerTag interface{} `json:"exclusive_consumer_tag"` + // Policy interface{} `json:"policy"` + ConsumerUtilisation float64 `json:"consumer_utilisation"` + // TODO use custom marshaller and parse into time.Time + IdleSince string `json:"idle_since"` + Memory int `json:"memory"` +} + +// RabbitBinding models the /bindings resource of the rabbitmq http api +type RabbitBinding struct { + Source string `json:"source"` + Vhost string `json:"vhost"` + Destination string `json:"destination"` + DestinationType string `json:"destination_type"` + RoutingKey string `json:"routing_key"` + Arguments map[string]interface{} `json:"arguments,omitempty"` + PropertiesKey string `json:"properties_key"` +} + +// IsExchangeToExchange returns true if this is an exchange-to-exchange binding +func (s RabbitBinding) IsExchangeToExchange() bool { + return s.DestinationType == "exchange" +} + +// RabbitExchange models the /exchanges resource of the rabbitmq http api +type RabbitExchange struct { + Name string `json:"name"` + Vhost string `json:"vhost"` + Type string `json:"type"` + Durable bool `json:"durable"` + AutoDelete bool `json:"auto_delete"` + Internal bool `json:"internal"` + Arguments map[string]interface{} `json:"arguments,omitempty"` + MessageStats struct { + PublishOut int `json:"publish_out"` + PublishOutDetails struct { + Rate float64 `json:"rate"` + } `json:"publish_out_details"` + PublishIn int `json:"publish_in"` + PublishInDetails struct { + Rate float64 `json:"rate"` + } `json:"publish_in_details"` + } `json:"message_stats,omitempty"` +} + +type OptInt int + +// ChannelDetails model channel_details in RabbitConsumer +type ChannelDetails struct { + PeerHost string `json:"peer_host"` + PeerPort OptInt `json:"peer_port"` + ConnectionName string `json:"connection_name"` + User string `json:"user"` + Number int `json:"number"` + Node string `json:"node"` + Name string `json:"name"` +} + +type ConnectionDetails struct { + PeerHost string `json:"peer_host"` + PeerPort OptInt `json:"peer_port"` + Name string `json:"name"` +} + +// RabbitConsumer models the /consumers resource of the rabbitmq http api +type RabbitConsumer struct { + // Arguments []interface{} `json:"arguments"` + PrefetchCount int `json:"prefetch_count"` + AckRequired bool `json:"ack_required"` + Active bool `json:"active"` + ActivityStatus string `json:"activity_status"` + Exclusive bool `json:"exclusive"` + ConsumerTag string `json:"consumer_tag"` + // see WORKAROUND above + ChannelDetails ChannelDetails `json:"channel_details,omitempty"` + Queue struct { + Vhost string `json:"vhost"` + Name string `json:"name"` + } `json:"queue"` +} diff --git a/pkg/rabbitmq_inmemory_metadata_service.go b/pkg/rabbitmq_inmemory_metadata_service.go new file mode 100644 index 0000000..46b8c41 --- /dev/null +++ b/pkg/rabbitmq_inmemory_metadata_service.go @@ -0,0 +1,149 @@ +// an implementation of MetadataService which uses in memory lookups +package rabtap + +import "fmt" + +type InMemoryMetadataService struct { + brokerInfo BrokerInfo + vhostByName map[string]*RabbitVhost + exchangeByName map[string]*RabbitExchange + queueByName map[string]*RabbitQueue + channelByName map[string]*RabbitChannel + connectionByName map[string]*RabbitConnection + channelsByConnection map[string][]*RabbitChannel + consumersByChannel map[string][]*RabbitConsumer + bindingsByExchange map[string][]*RabbitBinding +} + +func scopedName(vhost, name string) string { + return fmt.Sprintf("%s-%s", vhost, name) +} + +// entityByName returns a map[string]*T where all entities are referenced +// by their name, which is provided by nameFunc(T) +func entityByName[T any](entities []T, nameFunc func(T) string) map[string]*T { + res := make(map[string]*T, len(entities)) + for _, ent := range entities { + e := ent + res[nameFunc(e)] = &e + } + return res +} + +func findByName[T any](m map[string]*T, vhost, name string) *T { + return m[scopedName(vhost, name)] +} + +func channelsByConnection(channels []RabbitChannel) map[string][]*RabbitChannel { + chanByConn := map[string][]*RabbitChannel{} + for _, channel := range channels { + connName := scopedName(channel.Vhost, channel.ConnectionDetails.Name) + c := channel + chans := chanByConn[connName] + chanByConn[connName] = append(chans, &c) + } + return chanByConn +} + +func consumersByChannel(consumers []RabbitConsumer) map[string][]*RabbitConsumer { + consumerByChan := map[string][]*RabbitConsumer{} + for _, consumer := range consumers { + chanName := scopedName(consumer.Queue.Vhost, consumer.ChannelDetails.Name) + c := consumer + consumers := consumerByChan[chanName] + consumerByChan[chanName] = append(consumers, &c) + } + return consumerByChan +} + +func bindingsByExchange(bindings []RabbitBinding) map[string][]*RabbitBinding { + bindingsByExchange := map[string][]*RabbitBinding{} + for _, binding := range bindings { + exchangeName := scopedName(binding.Vhost, binding.Source) + b := binding + bindings := bindingsByExchange[exchangeName] + bindingsByExchange[exchangeName] = append(bindings, &b) + } + return bindingsByExchange +} + +func NewInMemoryMetadataService(brokerInfo BrokerInfo) *InMemoryMetadataService { + + return &InMemoryMetadataService{ + brokerInfo: brokerInfo, + vhostByName: entityByName(brokerInfo.Vhosts, func(v RabbitVhost) string { return scopedName(v.Name, "") }), + exchangeByName: entityByName(brokerInfo.Exchanges, func(e RabbitExchange) string { return scopedName(e.Vhost, e.Name) }), + queueByName: entityByName(brokerInfo.Queues, func(q RabbitQueue) string { return scopedName(q.Vhost, q.Name) }), + channelByName: entityByName(brokerInfo.Channels, func(c RabbitChannel) string { return scopedName(c.Vhost, c.Name) }), + connectionByName: entityByName(brokerInfo.Connections, func(c RabbitConnection) string { return scopedName(c.Vhost, c.Name) }), + channelsByConnection: channelsByConnection(brokerInfo.Channels), + consumersByChannel: consumersByChannel(brokerInfo.Consumers), + bindingsByExchange: bindingsByExchange(brokerInfo.Bindings), + } +} + +func (s InMemoryMetadataService) Overview() RabbitOverview { + return s.brokerInfo.Overview +} + +func (s InMemoryMetadataService) Connections() []RabbitConnection { + return s.brokerInfo.Connections +} + +func (s InMemoryMetadataService) Exchanges() []RabbitExchange { + return s.brokerInfo.Exchanges +} + +func (s InMemoryMetadataService) Queues() []RabbitQueue { + return s.brokerInfo.Queues +} + +func (s InMemoryMetadataService) Consumers() []RabbitConsumer { + return s.brokerInfo.Consumers +} + +func (s InMemoryMetadataService) Bindings() []RabbitBinding { + return s.brokerInfo.Bindings +} + +func (s InMemoryMetadataService) Channels() []RabbitChannel { + return s.brokerInfo.Channels +} + +func (s InMemoryMetadataService) Vhosts() []RabbitVhost { + return s.brokerInfo.Vhosts +} + +func (s InMemoryMetadataService) FindQueueByName(vhost, name string) *RabbitQueue { + return findByName(s.queueByName, vhost, name) +} + +func (s InMemoryMetadataService) FindVhostByName(vhost string) *RabbitVhost { + return findByName(s.vhostByName, vhost, "") +} + +func (s InMemoryMetadataService) FindExchangeByName(vhost, name string) *RabbitExchange { + return findByName(s.exchangeByName, vhost, name) +} + +func (s InMemoryMetadataService) FindChannelByName(vhost, name string) *RabbitChannel { + return findByName(s.channelByName, vhost, name) +} + +func (s InMemoryMetadataService) FindConnectionByName(vhost, name string) *RabbitConnection { + return findByName(s.connectionByName, vhost, name) +} + +func (s InMemoryMetadataService) AllChannelsForConnection(vhost, name string) []*RabbitChannel { + return s.channelsByConnection[scopedName(vhost, name)] +} + +func (s InMemoryMetadataService) AllConsumersForChannel(vhost, name string) []*RabbitConsumer { + return s.consumersByChannel[scopedName(vhost, name)] +} + +func (s InMemoryMetadataService) AllBindingsForExchange(vhost, name string) []*RabbitBinding { + return s.bindingsByExchange[scopedName(vhost, name)] +} + +var _ MetadataService = (*InMemoryMetadataService)(nil) diff --git a/pkg/rabbitmq_inmemory_metadata_service_test.go b/pkg/rabbitmq_inmemory_metadata_service_test.go new file mode 100644 index 0000000..711d91c --- /dev/null +++ b/pkg/rabbitmq_inmemory_metadata_service_test.go @@ -0,0 +1,41 @@ +package rabtap + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInMemoryMetadataServiceWorksWithEmptyBrokerInfo(t *testing.T) { + brokerInfo := BrokerInfo{ + Overview: RabbitOverview{}, + Connections: []RabbitConnection{}, + Exchanges: []RabbitExchange{}, + Queues: []RabbitQueue{}, + Consumers: []RabbitConsumer{}, + Bindings: []RabbitBinding{}, + Channels: []RabbitChannel{}, + Vhosts: []RabbitVhost{}, + } + metadataService := NewInMemoryMetadataService(brokerInfo) + + assert.Equal(t, RabbitOverview{}, metadataService.Overview()) + assert.Equal(t, []RabbitConnection{}, metadataService.Connections()) + assert.Equal(t, []RabbitExchange{}, metadataService.Exchanges()) + assert.Equal(t, []RabbitQueue{}, metadataService.Queues()) + assert.Equal(t, []RabbitConsumer{}, metadataService.Consumers()) + assert.Equal(t, []RabbitBinding{}, metadataService.Bindings()) + assert.Equal(t, []RabbitChannel{}, metadataService.Channels()) + assert.Equal(t, []RabbitVhost{}, metadataService.Vhosts()) + + assert.Equal(t, []*RabbitChannel(nil), metadataService.AllChannelsForConnection("vhost", "conn")) + assert.Equal(t, []*RabbitConsumer(nil), metadataService.AllConsumersForChannel("vhost", "cons")) + assert.Equal(t, []*RabbitBinding(nil), metadataService.AllBindingsForExchange("vhost", "exch")) + + assert.Nil(t, metadataService.FindQueueByName("vhost", "chan")) + assert.Nil(t, metadataService.FindVhostByName("vhost")) + assert.Nil(t, metadataService.FindExchangeByName("vhost", "exchange")) + assert.Nil(t, metadataService.FindChannelByName("vhost", "chan")) + assert.Nil(t, metadataService.FindConnectionByName("vhost", "conn")) + +} diff --git a/pkg/rabbitmq_metadata_service.go b/pkg/rabbitmq_metadata_service.go new file mode 100644 index 0000000..b588541 --- /dev/null +++ b/pkg/rabbitmq_metadata_service.go @@ -0,0 +1,23 @@ +// a service providing RabbitMQ metadata (queues, exchanges, connections...). +package rabtap + +type MetadataService interface { + Overview() RabbitOverview + Connections() []RabbitConnection + Exchanges() []RabbitExchange + Queues() []RabbitQueue + Consumers() []RabbitConsumer + Bindings() []RabbitBinding + Channels() []RabbitChannel + Vhosts() []RabbitVhost + + FindQueueByName(vhost, name string) *RabbitQueue + FindExchangeByName(vhost, name string) *RabbitExchange + FindChannelByName(vhost, name string) *RabbitChannel + FindConnectionByName(vhost, name string) *RabbitConnection + FindVhostByName(vhost string) *RabbitVhost + + AllChannelsForConnection(vhost, name string) []*RabbitChannel + AllConsumersForChannel(vhost, name string) []*RabbitConsumer + AllBindingsForExchange(vhost, name string) []*RabbitBinding +} diff --git a/pkg/rabbitmq_rest_client.go b/pkg/rabbitmq_rest_client.go index c7cfcfb..16259f4 100644 --- a/pkg/rabbitmq_rest_client.go +++ b/pkg/rabbitmq_rest_client.go @@ -1,9 +1,9 @@ -// Copyright (C) 2017 Jan Delgado +// rabbitmq http api client +// Copyright (C) 2017-2022 Jan Delgado package rabtap import ( - "bytes" "context" "crypto/tls" "encoding/json" @@ -30,7 +30,8 @@ type RabbitHTTPClient struct { // is the base API URL of the REST server. func NewRabbitHTTPClient(url *url.URL, tlsConfig *tls.Config) *RabbitHTTPClient { - tr := &http.Transport{TLSClientConfig: tlsConfig} + + tr := &http.Transport{TLSClientConfig: tlsConfig, DisableCompression: false} client := &http.Client{Transport: tr} return &RabbitHTTPClient{url, client} } @@ -55,15 +56,8 @@ func (s RabbitHTTPClient) getResource(ctx context.Context, request httpRequest) if resp.StatusCode != 200 { return r, errors.New(resp.Status) } - defer resp.Body.Close() - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(resp.Body) - if err != nil { - return r, err - } - - err = json.Unmarshal(buf.Bytes(), r) + err = json.NewDecoder(resp.Body).Decode(r) return r, err } @@ -86,7 +80,7 @@ func (s RabbitHTTPClient) delResource(ctx context.Context, path string) error { return nil } -// BrokerInfo represents the state of multiple RabbitMQ ressources as +// BrokerInfo represents the state of various RabbitMQ ressources as // returned by the RabbitMQ REST API type BrokerInfo struct { Overview RabbitOverview @@ -95,6 +89,8 @@ type BrokerInfo struct { Queues []RabbitQueue Consumers []RabbitConsumer Bindings []RabbitBinding + Channels []RabbitChannel + Vhosts []RabbitVhost } // Overview returns the /overview resource of the RabbitMQ REST API @@ -109,6 +105,12 @@ func (s RabbitHTTPClient) Connections(ctx context.Context) ([]RabbitConnection, return *res.(*[]RabbitConnection), err } +// Channels returns the /channels resource of the RabbitMQ REST API +func (s RabbitHTTPClient) Channels(ctx context.Context) ([]RabbitChannel, error) { + res, err := s.getResource(ctx, httpRequest{"channels", reflect.TypeOf([]RabbitChannel{})}) + return *res.(*[]RabbitChannel), err +} + // Exchanges returns the /exchanges resource of the RabbitMQ REST API func (s RabbitHTTPClient) Exchanges(ctx context.Context) ([]RabbitExchange, error) { res, err := s.getResource(ctx, httpRequest{"exchanges", reflect.TypeOf([]RabbitExchange{})}) @@ -133,18 +135,24 @@ func (s RabbitHTTPClient) Bindings(ctx context.Context) ([]RabbitBinding, error) return *res.(*[]RabbitBinding), err } +// Vhosts returns the /vhosts resource of the RabbitMQ REST API +func (s RabbitHTTPClient) Vhosts(ctx context.Context) ([]RabbitVhost, error) { + res, err := s.getResource(ctx, httpRequest{"vhosts", reflect.TypeOf([]RabbitVhost{})}) + return *res.(*[]RabbitVhost), err +} + // BrokerInfo gets all resources of the broker in parallel -// TODO use a ctx to for timeout/cancellation func (s RabbitHTTPClient) BrokerInfo(ctx context.Context) (BrokerInfo, error) { g, ctx := errgroup.WithContext(ctx) var r BrokerInfo - g.Go(func() (err error) { r.Overview, err = s.Overview(ctx); return }) g.Go(func() (err error) { r.Connections, err = s.Connections(ctx); return }) g.Go(func() (err error) { r.Exchanges, err = s.Exchanges(ctx); return }) g.Go(func() (err error) { r.Queues, err = s.Queues(ctx); return }) g.Go(func() (err error) { r.Consumers, err = s.Consumers(ctx); return }) g.Go(func() (err error) { r.Bindings, err = s.Bindings(ctx); return }) + g.Go(func() (err error) { r.Channels, err = s.Channels(ctx); return }) + g.Go(func() (err error) { r.Vhosts, err = s.Vhosts(ctx); return }) return r, g.Wait() } @@ -153,382 +161,6 @@ func (s RabbitHTTPClient) CloseConnection(ctx context.Context, conn, reason stri return s.delResource(ctx, "connections/"+conn) } -// FindQueueByName searches in the queues array for a queue with the given -// name and vhost. index is returned or -1 when nothing found. -func FindQueueByName(queues []RabbitQueue, - vhost, queueName string) int { - for i, queue := range queues { - if queue.Name == queueName && queue.Vhost == vhost { - return i - } - } - return -1 -} - -// FindExchangeByName searches in the exchanges array for an exchange with the given -// name and vhost. index is returned or -1 when nothing found. -func FindExchangeByName(exchanges []RabbitExchange, - vhost, exchangeName string) int { - for i, exchange := range exchanges { - if exchange.Name == exchangeName && exchange.Vhost == vhost { - return i - } - } - return -1 -} - -// UniqueVhosts returns the set of unique vhosts in the array of exchanges -func UniqueVhosts(exchanges []RabbitExchange) (vhosts map[string]bool) { - vhosts = make(map[string]bool) - for _, exchange := range exchanges { - vhosts[exchange.Vhost] = true - } - return -} - -// FindBindingsForExchange returns all bindings for a given exchange -func FindBindingsForExchange(exchange RabbitExchange, bindings []RabbitBinding) []RabbitBinding { - var result []RabbitBinding - for _, binding := range bindings { - if binding.Source == exchange.Name && - binding.Vhost == exchange.Vhost { - result = append(result, binding) - } - } - return result -} - -// currently not used. -// func FindChannelByName(channels []RabbitChannel, -// vhost, channelName string) int { -// for i, channel := range channels { -// if channel.Name == channelName && channel.Vhost == vhost { -// return i -// } -// } -// return -1 -// } - -// FindConnectionByName searches in the connections array for a connection with the given -// name and vhost. index is returned or -1 if nothing is found. -func FindConnectionByName(conns []RabbitConnection, - vhost, connName string) int { - for i, conn := range conns { - if conn.Name == connName && conn.Vhost == vhost { - return i - } - } - return -1 -} - -// FindConsumerByQueue searches in the connections array for a connection with the given -// name and vhost. index is returned or -1 if nothing is found. -func FindConsumerByQueue(consumers []RabbitConsumer, - vhost, queueName string) int { - for i, consumer := range consumers { - if consumer.Queue.Vhost == vhost && - consumer.Queue.Name == queueName { - return i - } - } - return -1 -} - -// RabbitConnection models the /connections resource of the rabbitmq http api -type RabbitConnection struct { - ReductionsDetails struct { - Rate float64 `json:"rate"` - } `json:"reductions_details"` - Reductions int `json:"reductions"` - RecvOctDetails struct { - Rate float64 `json:"rate"` - } `json:"recv_oct_details"` - RecvOct int `json:"recv_oct"` - SendOctDetails struct { - Rate float64 `json:"rate"` - } `json:"send_oct_details"` - SendOct int `json:"send_oct"` - ConnectedAt int64 `json:"connected_at"` - ClientProperties struct { - Product string `json:"product"` - Version string `json:"version"` - Capabilities struct { - ConnectionBlocked bool `json:"connection.blocked"` - ConsumerCancelNotify bool `json:"consumer_cancel_notify"` - } `json:"capabilities"` - } `json:"client_properties"` - ChannelMax int `json:"channel_max"` - FrameMax int `json:"frame_max"` - Timeout int `json:"timeout"` - Vhost string `json:"vhost"` - User string `json:"user"` - Protocol string `json:"protocol"` - SslHash interface{} `json:"ssl_hash"` - SslCipher interface{} `json:"ssl_cipher"` - SslKeyExchange interface{} `json:"ssl_key_exchange"` - SslProtocol interface{} `json:"ssl_protocol"` - AuthMechanism string `json:"auth_mechanism"` - PeerCertValidity interface{} `json:"peer_cert_validity"` - PeerCertIssuer interface{} `json:"peer_cert_issuer"` - PeerCertSubject interface{} `json:"peer_cert_subject"` - Ssl bool `json:"ssl"` - PeerHost string `json:"peer_host"` - Host string `json:"host"` - PeerPort int `json:"peer_port"` - Port int `json:"port"` - Name string `json:"name"` - Node string `json:"node"` - Type string `json:"type"` - GarbageCollection struct { - MinorGcs int `json:"minor_gcs"` - FullsweepAfter int `json:"fullsweep_after"` - MinHeapSize int `json:"min_heap_size"` - MinBinVheapSize int `json:"min_bin_vheap_size"` - MaxHeapSize int `json:"max_heap_size"` - } `json:"garbage_collection"` - Channels int `json:"channels"` - State string `json:"state"` - SendPend int `json:"send_pend"` - SendCnt int `json:"send_cnt"` - RecvCnt int `json:"recv_cnt"` -} - -// RabbitChannel models the /channels resource of the rabbitmq http api -type RabbitChannel struct { - ReductionsDetails struct { - Rate float64 `json:"rate"` - } `json:"reductions_details"` - Reductions int `json:"reductions"` - MessageStats struct { - ReturnUnroutableDetails struct { - Rate float64 `json:"rate"` - } `json:"return_unroutable_details"` - ReturnUnroutable int `json:"return_unroutable"` - ConfirmDetails struct { - Rate float64 `json:"rate"` - } `json:"confirm_details"` - Confirm int `json:"confirm"` - PublishDetails struct { - Rate float64 `json:"rate"` - } `json:"publish_details"` - Publish int `json:"publish"` - } `json:"message_stats"` - Vhost string `json:"vhost"` - User string `json:"user"` - Number int `json:"number"` - Name string `json:"name"` - Node string `json:"node"` - ConnectionDetails struct { - PeerHost string `json:"peer_host"` - PeerPort int `json:"peer_port"` - Name string `json:"name"` - } `json:"connection_details"` - GarbageCollection struct { - MinorGcs int `json:"minor_gcs"` - FullsweepAfter int `json:"fullsweep_after"` - MinHeapSize int `json:"min_heap_size"` - MinBinVheapSize int `json:"min_bin_vheap_size"` - MaxHeapSize int `json:"max_heap_size"` - } `json:"garbage_collection"` - State string `json:"state"` - GlobalPrefetchCount int `json:"global_prefetch_count"` - PrefetchCount int `json:"prefetch_count"` - AcksUncommitted int `json:"acks_uncommitted"` - MessagesUncommitted int `json:"messages_uncommitted"` - MessagesUnconfirmed int `json:"messages_unconfirmed"` - MessagesUnacknowledged int `json:"messages_unacknowledged"` - ConsumerCount int `json:"consumer_count"` - Confirm bool `json:"confirm"` - Transactional bool `json:"transactional"` -} - -// RabbitOverview models the /overview resource of the rabbitmq http api -type RabbitOverview struct { - ManagementVersion string `json:"management_version"` - RatesMode string `json:"rates_mode"` - ExchangeTypes []struct { - Name string `json:"name"` - Description string `json:"description"` - Enabled bool `json:"enabled"` - } `json:"exchange_types"` - RabbitmqVersion string `json:"rabbitmq_version"` - ClusterName string `json:"cluster_name"` - ErlangVersion string `json:"erlang_version"` - ErlangFullVersion string `json:"erlang_full_version"` - MessageStats struct { - DiskReads int `json:"disk_reads"` - DiskReadsDetails struct { - Rate float64 `json:"rate"` - } `json:"disk_reads_details"` - DiskWrites int `json:"disk_writes"` - DiskWritesDetails struct { - Rate float64 `json:"rate"` - } `json:"disk_writes_details"` - } `json:"message_stats"` - QueueTotals struct { - MessagesReady int `json:"messages_ready"` - MessagesReadyDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_ready_details"` - MessagesUnacknowledged int `json:"messages_unacknowledged"` - MessagesUnacknowledgedDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_unacknowledged_details"` - Messages int `json:"messages"` - MessagesDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_details"` - } `json:"queue_totals"` - ObjectTotals struct { - Consumers int `json:"consumers"` - Queues int `json:"queues"` - Exchanges int `json:"exchanges"` - Connections int `json:"connections"` - Channels int `json:"channels"` - } `json:"object_totals"` - StatisticsDbEventQueue int `json:"statistics_db_event_queue"` - Node string `json:"node"` - Listeners []struct { - Node string `json:"node"` - Protocol string `json:"protocol"` - IPAddress string `json:"ip_address"` - Port int `json:"port"` - // workaround for rabbitmq returning empty array OR valid object - // here. TODO check / further investigate.- - /*Dummy []interface{} `json:"socket_opts,omitempty"` - SocketOpts struct { - Backlog int `json:"backlog"` - Nodelay bool `json:"nodelay"` - //Linger []interface{} `json:"linger"` - ExitOnClose bool `json:"exit_on_close"` - } `json:"socket_opts"`*/ - } `json:"listeners"` - Contexts []struct { - Node string `json:"node"` - Description string `json:"description"` - Path string `json:"path"` - Port string `json:"port"` - Ssl string `json:"ssl"` - } `json:"contexts"` -} - -// RabbitQueue models the /queues resource of the rabbitmq http api -type RabbitQueue struct { - MessagesDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_details"` - Messages int `json:"messages"` - MessagesUnacknowledgedDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_unacknowledged_details"` - MessagesUnacknowledged int `json:"messages_unacknowledged"` - MessagesReadyDetails struct { - Rate float64 `json:"rate"` - } `json:"messages_ready_details"` - MessagesReady int `json:"messages_ready"` - ReductionsDetails struct { - Rate float64 `json:"rate"` - } `json:"reductions_details"` - Reductions int `json:"reductions"` - Node string `json:"node"` - Arguments struct { - } `json:"arguments"` - Exclusive bool `json:"exclusive"` - AutoDelete bool `json:"auto_delete"` - Durable bool `json:"durable"` - Vhost string `json:"vhost"` - Name string `json:"name"` - Type string `json:"type"` - MessageBytesPagedOut int `json:"message_bytes_paged_out"` - MessagesPagedOut int `json:"messages_paged_out"` - BackingQueueStatus struct { - Mode string `json:"mode"` - Q1 int `json:"q1"` - Q2 int `json:"q2"` - // Delta []interface{} `json:"delta"` - Q3 int `json:"q3"` - Q4 int `json:"q4"` - Len int `json:"len"` - // TargetRAMCount int `json:"target_ram_count"` // string or int -> need further research here when attr is in need ("infinity") - NextSeqID int `json:"next_seq_id"` - AvgIngressRate float64 `json:"avg_ingress_rate"` - AvgEgressRate float64 `json:"avg_egress_rate"` - AvgAckIngressRate float64 `json:"avg_ack_ingress_rate"` - AvgAckEgressRate float64 `json:"avg_ack_egress_rate"` - } `json:"backing_queue_status"` - // HeadMessageTimestamp interface{} `json:"head_message_timestamp"` - MessageBytesPersistent int `json:"message_bytes_persistent"` - MessageBytesRAM int `json:"message_bytes_ram"` - MessageBytesUnacknowledged int `json:"message_bytes_unacknowledged"` - MessageBytesReady int `json:"message_bytes_ready"` - MessageBytes int `json:"message_bytes"` - MessagesPersistent int `json:"messages_persistent"` - MessagesUnacknowledgedRAM int `json:"messages_unacknowledged_ram"` - MessagesReadyRAM int `json:"messages_ready_ram"` - MessagesRAM int `json:"messages_ram"` - GarbageCollection struct { - MinorGcs int `json:"minor_gcs"` - FullsweepAfter int `json:"fullsweep_after"` - MinHeapSize int `json:"min_heap_size"` - MinBinVheapSize int `json:"min_bin_vheap_size"` - MaxHeapSize int `json:"max_heap_size"` - } `json:"garbage_collection"` - State string `json:"state"` - // RecoverableSlaves interface{} `json:"recoverable_slaves"` - Consumers int `json:"consumers"` - // ExclusiveConsumerTag interface{} `json:"exclusive_consumer_tag"` - // Policy interface{} `json:"policy"` - ConsumerUtilisation float64 `json:"consumer_utilisation"` - // TODO use custom marshaller and parse into time.Time - IdleSince string `json:"idle_since"` - Memory int `json:"memory"` -} - -// RabbitBinding models the /bindings resource of the rabbitmq http api -type RabbitBinding struct { - Source string `json:"source"` - Vhost string `json:"vhost"` - Destination string `json:"destination"` - DestinationType string `json:"destination_type"` - RoutingKey string `json:"routing_key"` - Arguments map[string]interface{} `json:"arguments,omitempty"` - PropertiesKey string `json:"properties_key"` -} - -// RabbitExchange models the /exchanges resource of the rabbitmq http api -type RabbitExchange struct { - Name string `json:"name"` - Vhost string `json:"vhost"` - Type string `json:"type"` - Durable bool `json:"durable"` - AutoDelete bool `json:"auto_delete"` - Internal bool `json:"internal"` - Arguments map[string]interface{} `json:"arguments,omitempty"` - MessageStats struct { - PublishOut int `json:"publish_out"` - PublishOutDetails struct { - Rate float64 `json:"rate"` - } `json:"publish_out_details"` - PublishIn int `json:"publish_in"` - PublishInDetails struct { - Rate float64 `json:"rate"` - } `json:"publish_in_details"` - } `json:"message_stats,omitempty"` -} - -type OptInt int - -// ChannelDetails model channel_details in RabbitConsumer -type ChannelDetails struct { - PeerHost string `json:"peer_host"` - PeerPort OptInt `json:"peer_port"` - ConnectionName string `json:"connection_name"` - User string `json:"user"` - Number int `json:"number"` - Node string `json:"node"` - Name string `json:"name"` -} - // UnmarshalJSON is a workaround to deserialize int attributes in the // RabbitMQ API which are sometimes returned as strings, (i.e. the // value "undefined"). @@ -541,6 +173,17 @@ func (d *OptInt) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, aux) } +// helper for UnmarshalJSON. Unfortunately we can not use generic here and define +// an "type Alias T" here (see://go101.org/generics/888-the-status-quo-of-go-custom-generics.html) +// So some boiler plate is left in the UnmarshalJSON functions. +func unmarshalEmptyArrayOrObject(data []byte, v any) error { + if data[0] == '[' { + // JSON array detected + return nil + } + return json.Unmarshal(data, v) +} + // UnmarshalJSON is a custom unmarshaler as a WORKAROUND for RabbitMQ API // returning "[]" instead of null. To make sure deserialization does not // break, we catch this case, and return an empty ChannelDetails struct. @@ -553,24 +196,19 @@ func (d *ChannelDetails) UnmarshalJSON(data []byte) error { }{ Alias: (*Alias)(d), } - if data[0] == '[' { - // JSON array detected - return nil - } - return json.Unmarshal(data, aux) + return unmarshalEmptyArrayOrObject(data, &aux) } -// RabbitConsumer models the /consumers resource of the rabbitmq http api -type RabbitConsumer struct { - // Arguments []interface{} `json:"arguments"` - PrefetchCount int `json:"prefetch_count"` - AckRequired bool `json:"ack_required"` - Exclusive bool `json:"exclusive"` - ConsumerTag string `json:"consumer_tag"` - // see WORKAROUND above - ChannelDetails ChannelDetails `json:"channel_details,omitempty"` - Queue struct { - Vhost string `json:"vhost"` - Name string `json:"name"` - } `json:"queue"` +// UnmarshalJSON is a custom unmarshaler as a WORKAROUND for RabbitMQ API +// returning "[]" instead of null. To make sure deserialization does not +// break, we catch this case, and return an empty ChannelDetails struct. +// see e.g. https://github.com/rabbitmq/rabbitmq-management/issues/424 +func (d *ConnectionDetails) UnmarshalJSON(data []byte) error { + type Alias ConnectionDetails + aux := &struct { + *Alias + }{ + Alias: (*Alias)(d), + } + return unmarshalEmptyArrayOrObject(data, &aux) } diff --git a/pkg/rabbitmq_rest_client_test.go b/pkg/rabbitmq_rest_client_test.go index acfafe9..bd80687 100644 --- a/pkg/rabbitmq_rest_client_test.go +++ b/pkg/rabbitmq_rest_client_test.go @@ -205,7 +205,6 @@ func TestRabbitClientDeserializePeerPortInConsumerToInt(t *testing.T) { err := json.Unmarshal([]byte(msg), &consumer) assert.NoError(t, err) assert.Equal(t, OptInt(1234), consumer[0].ChannelDetails.PeerPort) - } func TestRabbitClientDeserializePeerPortInConsumerAsStringWithoutError(t *testing.T) { @@ -244,22 +243,63 @@ func TestRabbitClientDeserializePeerPortInConsumerAsStringWithoutError(t *testin } -// test of GET /api/consumers endpoint workaround for empty channel_details -func TestRabbitClientGetConsumersChannelDetailsIsEmptyArray(t *testing.T) { - - mock := testcommon.NewRabbitAPIMock(testcommon.MockModeStd) - defer mock.Close() - url, _ := url.Parse(mock.URL) - client := NewRabbitHTTPClient(url, &tls.Config{}) +// we use a custom unmarshaler as a WORKAROUND for RabbitMQ API +// returning "[]" instead of null. To make sure deserialization does not +// break, we catch this case, and return an empty ChannelDetails struct. +// see e.g. https://github.com/rabbitmq/rabbitmq-management/issues/424 +func TestChannelDetailsIsDetectedAsNull(t *testing.T) { + msg := ` +[ + { + "arguments": {}, + "ack_required": true, + "active": true, + "activity_status": "up", + "channel_details": null, + "consumer_tag": "amq.ctag-InRAvLn4GW3j2mRwPmWJxA", + "exclusive": false, + "prefetch_count": 20, + "queue": { + "name": "logstream", + "vhost": "/" + } + } +] +` + var consumer []RabbitConsumer + err := json.Unmarshal([]byte(msg), &consumer) + assert.NoError(t, err) + assert.Equal(t, ChannelDetails{}, consumer[0].ChannelDetails) - consumer, err := client.Consumers(context.TODO()) - assert.Nil(t, err) - assert.Equal(t, 2, len(consumer)) +} - // the second channel_details were "[]" to test behaviour of RabbitMQ - // api when [] is returned instead of a null object. - assert.Equal(t, "another_consumer w/ faulty channel", consumer[1].ConsumerTag) - assert.Equal(t, "", consumer[1].ChannelDetails.Name) +// we use a custom unmarshaler as a WORKAROUND for RabbitMQ API +// returning "[]" instead of null. To make sure deserialization does not +// break, we catch this case, and return an empty ChannelDetails struct. +// see e.g. https://github.com/rabbitmq/rabbitmq-management/issues/424 +func TestChannelDetailsIsDetectedAsEmptyArray(t *testing.T) { + msg := ` +[ + { + "arguments": {}, + "ack_required": true, + "active": true, + "activity_status": "up", + "channel_details": [], + "consumer_tag": "amq.ctag-InRAvLn4GW3j2mRwPmWJxA", + "exclusive": false, + "prefetch_count": 20, + "queue": { + "name": "logstream", + "vhost": "/" + } + } +] +` + var consumer []RabbitConsumer + err := json.Unmarshal([]byte(msg), &consumer) + assert.NoError(t, err) + assert.Equal(t, ChannelDetails{}, consumer[0].ChannelDetails) } // test of DELETE /connections/conn to close a connection @@ -286,90 +326,3 @@ func TestRabbitClientCloseNonExistingConnectionRaisesError(t *testing.T) { err := client.CloseConnection(context.TODO(), "DOES NOT EXIST", "reason") assert.NotNil(t, err) } - -func TestFindExchangeByName(t *testing.T) { - exchanges := []RabbitExchange{ - {Name: "exchange1", Vhost: "vhost"}, - {Name: "exchange2", Vhost: "vhost"}, - } - assert.Equal(t, 1, FindExchangeByName(exchanges, "vhost", "exchange2")) -} - -func TestFindExchangeByNameNotFound(t *testing.T) { - exchanges := []RabbitExchange{ - {Name: "exchange1", Vhost: "vhost"}, - } - assert.Equal(t, -1, FindExchangeByName(exchanges, "/", "not-available")) -} - -func TestFindQueueByName(t *testing.T) { - queues := []RabbitQueue{ - {Name: "q1", Vhost: "vhost"}, - {Name: "q2", Vhost: "vhost"}, - } - assert.Equal(t, 1, FindQueueByName(queues, "vhost", "q2")) -} - -func TestFindQueueByNameNotFound(t *testing.T) { - queues := []RabbitQueue{ - {Name: "q1", Vhost: "vhost"}, - {Name: "q2", Vhost: "vhost"}, - } - assert.Equal(t, -1, FindQueueByName(queues, "/", "not-available")) -} - -func TestFindConnectionByName(t *testing.T) { - conns := []RabbitConnection{ - {Name: "c1", Vhost: "vhost"}, - {Name: "c2", Vhost: "vhost"}, - } - assert.Equal(t, 1, FindConnectionByName(conns, "vhost", "c2")) -} - -func TestFindConnectionByNameNotFoundReturnsCorrectValue(t *testing.T) { - assert.Equal(t, -1, FindConnectionByName([]RabbitConnection{}, "vhost", "c2")) -} - -func TestFindConsumerByQueue(t *testing.T) { - var con1, con2, con3 RabbitConsumer - con1.Queue.Name = "q1" - con1.Queue.Vhost = "vhost" - con2.Queue.Name = "q2" - con2.Queue.Vhost = "vhost" - con3.Queue.Name = "q3" - con3.Queue.Vhost = "vhost" - cons := []RabbitConsumer{con1, con2, con3} - assert.Equal(t, 1, FindConsumerByQueue(cons, "vhost", "q2")) -} - -func TestFindConsumerByQueueNotFoundReturnsCorrectValue(t *testing.T) { - assert.Equal(t, -1, FindConsumerByQueue([]RabbitConsumer{}, "vhost", "q1")) -} - -func TestUniqueVhostsReturnsUniqueMapOfVhosts(t *testing.T) { - exchanges := []RabbitExchange{ - {Name: "e1", Vhost: "vhost1"}, - {Name: "e2", Vhost: "vhost1"}, - {Name: "e3", Vhost: "vhost2"}, - {Name: "e4", Vhost: "vhost3"}, - } - // expect map[string]bool returned with 3 entries - vhosts := UniqueVhosts(exchanges) - assert.Equal(t, 3, len(vhosts)) - assert.True(t, vhosts["vhost1"]) - assert.True(t, vhosts["vhost2"]) - assert.True(t, vhosts["vhost3"]) -} - -func TestFindBindingsByExchangeReturnsMatchingBindings(t *testing.T) { - bindings := []RabbitBinding{ - {Source: "e1", Vhost: "vh1", Destination: "q1"}, - {Source: "e2", Vhost: "vh2", Destination: "q2"}, - {Source: "e1", Vhost: "vh1", Destination: "q3"}, - } - exchange := RabbitExchange{Name: "e1", Vhost: "vh1"} - foundBindings := FindBindingsForExchange(exchange, bindings) - assert.Equal(t, 2, len(foundBindings)) - assert.Equal(t, "q1", foundBindings[0].Destination) - assert.Equal(t, "q3", foundBindings[1].Destination) -} diff --git a/pkg/testcommon/rabbitmq_rest_api_mock.go b/pkg/testcommon/rabbitmq_rest_api_mock.go index 9f0f4ba..41bd728 100644 --- a/pkg/testcommon/rabbitmq_rest_api_mock.go +++ b/pkg/testcommon/rabbitmq_rest_api_mock.go @@ -60,6 +60,8 @@ func mockStdHandler(w http.ResponseWriter, r *http.Request) { func mockStdGetHandler(w http.ResponseWriter, r *http.Request) { result := "" switch r.URL.RequestURI() { + case "/vhosts": + result = vhostsResult case "/exchanges": result = exchangeResult case "/bindings": @@ -93,6 +95,9 @@ func mockStdDeleteHandler(w http.ResponseWriter, r *http.Request) { } const ( + vhostsResult = ` +[{"cluster_state":{"rabbit@108f57d1fe8ab":"running"},"description":"","message_stats":{"ack":13187,"ack_details":{"rate":0.0},"confirm":0,"confirm_details":{"rate":0.0},"deliver":13190,"deliver_details":{"rate":0.0},"deliver_get":13190,"deliver_get_details":{"rate":0.0},"deliver_no_ack":0,"deliver_no_ack_details":{"rate":0.0},"drop_unroutable":1,"drop_unroutable_details":{"rate":0.0},"get":0,"get_details":{"rate":0.0},"get_empty":0,"get_empty_details":{"rate":0.0},"get_no_ack":0,"get_no_ack_details":{"rate":0.0},"publish":109674,"publish_details":{"rate":0.0},"redeliver":0,"redeliver_details":{"rate":0.0},"return_unroutable":0,"return_unroutable_details":{"rate":0.0}},"messages":4,"messages_details":{"rate":0.0},"messages_ready":4,"messages_ready_details":{"rate":0.0},"messages_unacknowledged":0,"messages_unacknowledged_details":{"rate":0.0},"metadata":{"description":"","tags":[]},"name":"/","recv_oct":31769538,"recv_oct_details":{"rate":3.2},"send_oct":4738294,"send_oct_details":{"rate":3.2},"tags":[],"tracing":false}]` + bindingResult = ` [ { @@ -272,11 +277,11 @@ const ( "properties_key": "topic-q2" }, { - "source": "test-topic", + "source": "amq.topic", "vhost": "/", "destination": "test-topic", "destination_type": "exchange", - "routing_key": "", + "routing_key": "test", "arguments": { }, diff --git a/pkg/testcommon/test_common.go b/pkg/testcommon/test_common.go index a257fe6..1d7abe6 100644 --- a/pkg/testcommon/test_common.go +++ b/pkg/testcommon/test_common.go @@ -7,6 +7,7 @@ package testcommon import ( "bytes" + "context" "fmt" "io" "log" @@ -150,7 +151,8 @@ func PublishTestMessages(t *testing.T, ch *amqp.Channel, numMessages int, // in the tap-exchange defined above. for i := 1; i <= numMessages; i++ { // publish the test message - err := ch.Publish( + err := ch.PublishWithContext( + context.TODO(), exchangeName, routingKey, false, // mandatory