rabbitmq-graph 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +81 -0
- data/bin/rabbitmq-graph +75 -0
- data/lib/rabbitmq-graph.rb +1 -0
- data/lib/rabbitmq-graph/discover.rb +136 -0
- data/lib/rabbitmq-graph/dot_format.rb +85 -0
- data/lib/rabbitmq-graph/markdown_table_format.rb +47 -0
- data/lib/rabbitmq-graph/route.rb +58 -0
- data/rabbitmq-graph.gemspec +18 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0c207d8f0935bddfd81f0e6600182134797df3a27f283a4b71ac172f8ea23549
|
4
|
+
data.tar.gz: 75690fe90096674172429863f3bb21b71a9b11cfa1014fbd1f84acf4d85fded0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f381610c65e306db2d734852f21d3cd23932747418cea7437d8ceffbc94a02feec3f8215ff4f95910e0b7209c1ff2bb4b2119ca8c9a8ee0d6c87707e7478668f
|
7
|
+
data.tar.gz: 9a2013484acbac11866864c1095b8a795210ac8e1d6c77478fd869ae5e535dd7a40a1ebdcc057c7208aa71e9ae4e3e800a8aa3a3ec06f168c542edfd1bf8cb48
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# rabbitmq-graph
|
2
|
+
|
3
|
+
[![Dependency Status](https://gemnasium.com/badges/github.com/sldblog/rabbitmq-graph.svg)](https://gemnasium.com/github.com/sldblog/rabbitmq-graph)
|
4
|
+
[![CircleCI](https://circleci.com/gh/sldblog/rabbitmq-graph.svg?style=svg&circle-token=68531f42debaa4ff5b3bddb62a4672ca2eaabaf4)](https://circleci.com/gh/sldblog/rabbitmq-graph)
|
5
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/146dab10c24b4dd7b75e/maintainability)](https://codeclimate.com/github/sldblog/rabbitmq-graph/maintainability)
|
6
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/146dab10c24b4dd7b75e/test_coverage)](https://codeclimate.com/github/sldblog/rabbitmq-graph/test_coverage)
|
7
|
+
|
8
|
+
Discover RabbitMQ topology.
|
9
|
+
|
10
|
+
## Assumptions
|
11
|
+
|
12
|
+
- Routing keys are segmented with dots (`.`).
|
13
|
+
|
14
|
+
| Segment name | Routing key | Extracted | Assumed to be |
|
15
|
+
| --- | --- | --- | --- |
|
16
|
+
| from\_app | **splitter**.experiment.something.assigned | splitted | The name of the publishing application. |
|
17
|
+
| entity | splitter.**experiment**.something.assigned | experiment | The entity that is participating in the action. |
|
18
|
+
| actions | splitter.experiment.**something.assigned** | something.assigned | The action(s) describing the event. |
|
19
|
+
|
20
|
+
- [Consumer tags][hutch-consumer-tag-pr] are configured to contain the name of the consuming application.
|
21
|
+
|
22
|
+
## How to run?
|
23
|
+
|
24
|
+
Without arguments `bin/rabbitmq-graph` will connect to `localhost:15672` with the default guest user.
|
25
|
+
|
26
|
+
### Configuration
|
27
|
+
|
28
|
+
| Setting | Configuration | Effect | Default |
|
29
|
+
| ------- | ------------- | ------ | ------- |
|
30
|
+
| RabbitMQ management URL | `-uURL`<br/>`--url=URL`<br/>or environment variable<br/>`RABBITMQ_API_URI` | Specifies the connection URL to RabbitMQ management API | http://guest:guest@localhost:15672/ |
|
31
|
+
| Save topology | `--save-topology=FILE` | After discovery save the topology to the given file. | disabled |
|
32
|
+
| Read topology | `--read-topology=FILE` | Skip discovery and use a stored topology file. | disabled |
|
33
|
+
| Choose format | `--format=FORMAT` | Choose an output format. `--help` will give a list of available options. | `DotFormat` |
|
34
|
+
|
35
|
+
#### Dot format specific options
|
36
|
+
|
37
|
+
| Setting | Configuration | Effect | Default |
|
38
|
+
| ------- | ------------- | ------ | ------- |
|
39
|
+
| Show only applications | `--dot-applications-only` | Creates a graph without entity nodes. | disabled |
|
40
|
+
| Label details | `--dot-label-detail=DETAILS` | Comma separated segment names to display on labels drawn between applications and/or entities. | `'actions'` |
|
41
|
+
|
42
|
+
### Show only applications
|
43
|
+
|
44
|
+
- **enabled**: will only show application to application relations.
|
45
|
+
- **disabled** (default): will show application to entity to application relations. The edge going into the entity and coming out of the entity will have the same label.
|
46
|
+
|
47
|
+
### Label details
|
48
|
+
|
49
|
+
Affects the labeling of edges:
|
50
|
+
|
51
|
+
- `'entity,actions'`: displays the entity name and the actions on the edge.
|
52
|
+
- `'entity'`: displays the entity name on the edge.
|
53
|
+
- `'actions'`: displays the actions on the edge.
|
54
|
+
- `''` (empty string): displays no labels.
|
55
|
+
|
56
|
+
Any combination and order of the above is allowed.
|
57
|
+
|
58
|
+
### Example
|
59
|
+
|
60
|
+
Running the discovery against a dockerised `rabbitmq:3.6-management`:
|
61
|
+
|
62
|
+
```
|
63
|
+
$ docker run --detach --publish 5672:5672 --publish 15672:15672 rabbitmq:3.6-management
|
64
|
+
|
65
|
+
$ RABBITMQ_API_URI=http://localhost:15672/ bin/rabbitmq-graph > test.dot
|
66
|
+
I, [2018-04-30T13:05:29.735060 #90042] INFO -- : connecting to rabbitmq HTTP API (http://guest@127.0.0.1:15672/)
|
67
|
+
Discovering bindings: |================================================================================================|
|
68
|
+
Discovering queues: |==================================================================================================|
|
69
|
+
|
70
|
+
$ fdp -O -Tpng test.dot # assumes "graphviz" is installed
|
71
|
+
$ open test.dot.png
|
72
|
+
```
|
73
|
+
|
74
|
+
## How to configure consumer tags?
|
75
|
+
|
76
|
+
### hutch
|
77
|
+
|
78
|
+
Hutch supports consumer tag prefixes since [0.24][hutch-0.24].
|
79
|
+
|
80
|
+
[hutch-consumer-tag-pr]: https://github.com/gocardless/hutch/pull/265
|
81
|
+
[hutch-0.24]: https://github.com/gocardless/hutch/blob/master/CHANGELOG.md#0240--february-1st-2017
|
data/bin/rabbitmq-graph
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'optparse'
|
6
|
+
require 'rabbitmq-graph/discover'
|
7
|
+
require 'rabbitmq-graph/dot_format'
|
8
|
+
require 'rabbitmq-graph/markdown_table_format'
|
9
|
+
require 'rabbitmq-graph/route'
|
10
|
+
|
11
|
+
def topology(options)
|
12
|
+
if (read_file = options[:discover][:read_topology_file])
|
13
|
+
JSON.parse(IO.read(read_file), symbolize_names: true).map { |route_hash| Route.new(route_hash) }
|
14
|
+
else
|
15
|
+
result = Discover.new(api_url: options[:discover][:api_url]).topology
|
16
|
+
if (save_file = options[:discover][:save_topology_file])
|
17
|
+
output = "[\n " + result.map { |e| JSON.generate(e.to_h) }.join(",\n ") + "\n]\n"
|
18
|
+
IO.write(save_file, output)
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup_discovery_options(options, option_parser)
|
25
|
+
options[:discover] ||= {}
|
26
|
+
options[:discover][:api_url] = ENV['RABBITMQ_API_URI'] || 'http://guest:guest@localhost:15672/'
|
27
|
+
option_parser.on('-uURL', '--url=URL', 'RabbitMQ management API URL. ' \
|
28
|
+
'Defaults to "http://guest:guest@localhost:15672/". ' \
|
29
|
+
'Also configurable through the "RABBITMQ_API_URI" environment variable.') do |url|
|
30
|
+
options[:discover][:api_url] = url
|
31
|
+
end
|
32
|
+
option_parser.on('--read-topology=FILE', 'Skip discovery and use a stored topology file.') do |file|
|
33
|
+
options[:discover][:read_topology_file] = file
|
34
|
+
end
|
35
|
+
option_parser.on('--save-topology=FILE', 'After discovery save the topology to the given file.') do |file|
|
36
|
+
options[:discover][:save_topology_file] = file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_format_options(options, option_parser, formats:)
|
41
|
+
options[:format] = formats.first
|
42
|
+
option_parser.on('--format=FORMAT', formats.map(&:to_s), "Select format to use from #{formats.join(', ')}. " \
|
43
|
+
"Defaults to #{options[:format]}.") do |format|
|
44
|
+
options[:format] = Object.const_get(format)
|
45
|
+
end
|
46
|
+
|
47
|
+
formats.each { |format| options[format] = {} }
|
48
|
+
|
49
|
+
# DotFormat specific options
|
50
|
+
options[DotFormat][:show_entities] = true
|
51
|
+
option_parser.on('--dot-applications-only', 'Creates a graph without entity nodes.') do |apps_only|
|
52
|
+
options[DotFormat][:show_entities] = !apps_only
|
53
|
+
end
|
54
|
+
|
55
|
+
options[DotFormat][:label_detail] = %i[actions]
|
56
|
+
option_parser.on('--dot-label-detail=DETAILS', 'Specifies edge label format. ' \
|
57
|
+
'Comma separated list of "queue_name", "entity", "actions"') do |label_detail|
|
58
|
+
options[DotFormat][:label_detail] = label_detail.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
options = {}
|
63
|
+
OptionParser.new do |option_parser|
|
64
|
+
option_parser.banner = "Usage: #{File.basename(__FILE__)} [options]"
|
65
|
+
setup_discovery_options(options, option_parser)
|
66
|
+
setup_format_options(options, option_parser, formats: [DotFormat, MarkdownTableFormat])
|
67
|
+
option_parser.on('-h', '--help', 'Prints this help.') do
|
68
|
+
puts option_parser
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
end.parse!
|
72
|
+
|
73
|
+
chosen_format = options[:format]
|
74
|
+
format_options = options[chosen_format].merge(topology: topology(options))
|
75
|
+
puts chosen_format.new(format_options).present
|
@@ -0,0 +1 @@
|
|
1
|
+
# frozen_string_literal: true
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hutch'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
require 'ruby-progressbar'
|
7
|
+
require 'uri'
|
8
|
+
require 'rabbitmq-graph/route'
|
9
|
+
|
10
|
+
# Using RabbitMQ's HTTP management API, discovers the server's publisher/subscriber topology.
|
11
|
+
#
|
12
|
+
# Assumes that:
|
13
|
+
# - `consumer_tag`s are set on consumers to the consuming application's name
|
14
|
+
# - bound routing keys are in the format of `application_name.entity_name[.action]+`
|
15
|
+
class Discover
|
16
|
+
def initialize(api_url: ENV.fetch('RABBITMQ_API_URI', 'http://guest:guest@localhost:15672/'),
|
17
|
+
api_client: nil, output: $stderr)
|
18
|
+
@output_io = output
|
19
|
+
Hutch::Logging.logger = Logger.new(output_io)
|
20
|
+
configure_hutch_http_api(api_url)
|
21
|
+
configure_api_client(api_client)
|
22
|
+
end
|
23
|
+
|
24
|
+
def topology
|
25
|
+
all_publishers = discover_routing_keys
|
26
|
+
all_consumers = discover_queues_and_consumers
|
27
|
+
queue_names = all_publishers.keys | all_consumers.keys
|
28
|
+
|
29
|
+
queue_names.inject([]) do |result, queue_name|
|
30
|
+
publishers = all_publishers[queue_name] || []
|
31
|
+
publishers << {} if publishers.empty?
|
32
|
+
consumers = all_consumers[queue_name] || []
|
33
|
+
consumers << {} if consumers.empty?
|
34
|
+
|
35
|
+
routes = publishers
|
36
|
+
.flat_map { |route| consumers.map { |consumer| route.merge(consumer) } }
|
37
|
+
.map { |route| route.delete_if { |_key, value| value.nil? } }
|
38
|
+
.map { |route| Route.new(route.merge(queue_name: queue_name)) }
|
39
|
+
.uniq
|
40
|
+
result.concat(routes)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :client, :output_io
|
47
|
+
|
48
|
+
def configure_hutch_http_api(api_url)
|
49
|
+
parsed_uri = URI(api_url)
|
50
|
+
Hutch::Config.set(:mq_api_host, parsed_uri.host)
|
51
|
+
Hutch::Config.set(:mq_username, parsed_uri.user || 'guest')
|
52
|
+
Hutch::Config.set(:mq_password, parsed_uri.password || 'guest')
|
53
|
+
Hutch::Config.set(:mq_api_port, parsed_uri.port)
|
54
|
+
Hutch::Config.set(:mq_api_ssl, parsed_uri.scheme == 'https')
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure_api_client(api_client)
|
58
|
+
return @client = api_client if api_client
|
59
|
+
|
60
|
+
broker = Hutch::Broker.new
|
61
|
+
broker.set_up_api_connection
|
62
|
+
@client = broker.api_client
|
63
|
+
end
|
64
|
+
|
65
|
+
def discover_routing_keys
|
66
|
+
items = {}
|
67
|
+
ProgressBar.create(title: 'Discovering bindings', total: client.bindings.size, output: output_io).tap do |progress|
|
68
|
+
bindings.each do |mq_binding|
|
69
|
+
queue_name = mq_binding[:queue_name]
|
70
|
+
items[queue_name] ||= []
|
71
|
+
items[queue_name] << { routing_key: mq_binding[:routing_key] }
|
72
|
+
progress.increment
|
73
|
+
end
|
74
|
+
progress.finish
|
75
|
+
end
|
76
|
+
items
|
77
|
+
end
|
78
|
+
|
79
|
+
def discover_queues_and_consumers
|
80
|
+
items = {}
|
81
|
+
ProgressBar.create(title: 'Discovering queues', total: client.queues.size, output: output_io).tap do |progress|
|
82
|
+
bound_queues.each do |queue|
|
83
|
+
bound_consumers(queue).each do |consumer|
|
84
|
+
queue_name = consumer[:queue_name]
|
85
|
+
items[queue_name] ||= []
|
86
|
+
items[queue_name] << { consumer_tag: consumer[:consumer] }
|
87
|
+
end
|
88
|
+
progress.increment
|
89
|
+
end
|
90
|
+
progress.finish
|
91
|
+
end
|
92
|
+
items
|
93
|
+
end
|
94
|
+
|
95
|
+
def bindings
|
96
|
+
client.bindings.lazy
|
97
|
+
.select { |binding| binding['destination_type'] == 'queue' }
|
98
|
+
.reject { |binding| binding['routing_key'].empty? }
|
99
|
+
.reject { |binding| binding['source'].empty? }
|
100
|
+
.map { |binding_data| extract_binding_data(binding_data) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def bound_queues
|
104
|
+
client.queues.lazy
|
105
|
+
.map { |queue| fetch_queue_data(queue['vhost'], queue['name']) }
|
106
|
+
.map { |queue| queue['consumer_details'] }
|
107
|
+
end
|
108
|
+
|
109
|
+
def bound_consumers(queue_data)
|
110
|
+
queue_data
|
111
|
+
.flatten
|
112
|
+
.reject(&:empty?)
|
113
|
+
.map { |consumer_data| extract_consumer_data(consumer_data) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def extract_binding_data(binding_data)
|
117
|
+
{
|
118
|
+
vhost: binding_data['vhost'],
|
119
|
+
queue_name: binding_data['destination'],
|
120
|
+
routing_key: binding_data['routing_key']
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def extract_consumer_data(consumer_data)
|
125
|
+
{
|
126
|
+
vhost: consumer_data['queue']['vhost'],
|
127
|
+
queue_name: consumer_data['queue']['name'],
|
128
|
+
consumer: consumer_data['consumer_tag']
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
def fetch_queue_data(vhost, name)
|
133
|
+
escaped_vhost_path = URI.encode_www_form_component(vhost)
|
134
|
+
JSON.parse(client.query_api(path: "/queues/#{escaped_vhost_path}/#{name}").body)
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
# Presents a RabbitMQ topology in graphviz's .dot format
|
6
|
+
class DotFormat
|
7
|
+
def initialize(topology:, show_entities: true, label_detail: %i[actions])
|
8
|
+
@topology = topology
|
9
|
+
@show_entities = show_entities
|
10
|
+
@label_detail = label_detail
|
11
|
+
end
|
12
|
+
|
13
|
+
def present
|
14
|
+
<<-GRAPH
|
15
|
+
digraph G {
|
16
|
+
#{render_application_subgraph}
|
17
|
+
#{render_entity_subgraph}
|
18
|
+
#{message_edges.join("\n")}
|
19
|
+
}
|
20
|
+
GRAPH
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :topology, :show_entities, :label_detail
|
26
|
+
|
27
|
+
def render_application_subgraph
|
28
|
+
<<-APPS
|
29
|
+
subgraph Apps {
|
30
|
+
node [shape=oval fillcolor=yellow style=filled]
|
31
|
+
#{application_nodes.join("\n")}
|
32
|
+
}
|
33
|
+
APPS
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_entity_subgraph
|
37
|
+
return '' unless show_entities
|
38
|
+
<<-ENTITIES
|
39
|
+
subgraph Entities {
|
40
|
+
node [shape=box fillcolor=turquoise style=filled]
|
41
|
+
#{entity_nodes.join("\n")}
|
42
|
+
}
|
43
|
+
ENTITIES
|
44
|
+
end
|
45
|
+
|
46
|
+
def application_nodes
|
47
|
+
applications = {}
|
48
|
+
topology.each do |route|
|
49
|
+
applications[route.source_app] ||= Set.new
|
50
|
+
applications[route.source_app] << 'fillcolor="red"' if route.missing_source?
|
51
|
+
|
52
|
+
applications[route.target_app] ||= Set.new
|
53
|
+
applications[route.target_app] << 'fillcolor="red"' if route.missing_target?
|
54
|
+
applications[route.target_app] << 'fillcolor="orange"' if route.default_consumer_tag?
|
55
|
+
end
|
56
|
+
applications.map { |name, properties| %("#{name}" [#{properties.to_a.join(' ')}]) }.sort
|
57
|
+
end
|
58
|
+
|
59
|
+
def entity_nodes
|
60
|
+
entities = topology.map(&:entity).sort.uniq
|
61
|
+
entities.map { |entity| %("#{entity}") }
|
62
|
+
end
|
63
|
+
|
64
|
+
def message_edges
|
65
|
+
topology.map { |route| %(#{route_path(route)} [#{route_properties(route)}]) }.uniq
|
66
|
+
end
|
67
|
+
|
68
|
+
def route_path(route)
|
69
|
+
path = []
|
70
|
+
path << route.source_app
|
71
|
+
path << route.entity if show_entities
|
72
|
+
path << route.target_app
|
73
|
+
path.map { |text| %("#{text}") }.join('->')
|
74
|
+
end
|
75
|
+
|
76
|
+
def route_properties(route)
|
77
|
+
label = label_detail.select { |detail| route.respond_to?(detail) }
|
78
|
+
.map { |detail| route.public_send(detail) }
|
79
|
+
.flatten.join('.')
|
80
|
+
properties = []
|
81
|
+
properties << %(label="#{label}")
|
82
|
+
properties << %(color="red") if route.missing_source? || route.missing_target?
|
83
|
+
properties.join(' ')
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Presents a RabbitMQ topology in a GitHub-flavoured Markdown table
|
4
|
+
class MarkdownTableFormat
|
5
|
+
def initialize(topology:)
|
6
|
+
@topology = topology
|
7
|
+
end
|
8
|
+
|
9
|
+
def present
|
10
|
+
no_consumer_routes = topology.select(&:missing_target?)
|
11
|
+
no_binding_routes = topology.select(&:missing_source?)
|
12
|
+
default_tag_routes = topology.select(&:default_consumer_tag?)
|
13
|
+
connected_routes = topology.reject { |r| r.missing_source? || r.missing_target? || r.default_consumer_tag? }
|
14
|
+
|
15
|
+
lines = []
|
16
|
+
lines.concat(route_table('Routes without consumers', no_consumer_routes))
|
17
|
+
lines.concat(route_table('Routes without publisher bindings', no_binding_routes))
|
18
|
+
lines.concat(route_table('Routes with default consumer names', default_tag_routes))
|
19
|
+
lines.concat(route_table('Named, connected routes', connected_routes))
|
20
|
+
lines.join("\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :topology
|
26
|
+
|
27
|
+
def route_table(title, routes)
|
28
|
+
return [] if routes.empty?
|
29
|
+
lines = []
|
30
|
+
lines << ''
|
31
|
+
lines << "# #{title}"
|
32
|
+
lines << ''
|
33
|
+
lines << '| Publisher application | Consumer application | Entity | Actions | Queue |'
|
34
|
+
lines << '| --- | --- | --- | --- | --- |'
|
35
|
+
lines.concat(routes.map { |route| route_line(route) }.uniq)
|
36
|
+
end
|
37
|
+
|
38
|
+
def route_line(route)
|
39
|
+
columns = []
|
40
|
+
columns << route.source_app
|
41
|
+
columns << route.target_app
|
42
|
+
columns << route.entity
|
43
|
+
columns << route.actions.join('.')
|
44
|
+
columns << route.queue_name
|
45
|
+
'| ' + columns.join(' | ') + ' |'
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Extracts publisher/consumer application names and routing key fragments from routing data
|
4
|
+
class Route
|
5
|
+
DEFAULT_CONSUMER_TAG = 'default-consumer-tag'
|
6
|
+
MISSING_SOURCE_LABEL = 'no-routing-key-binding'
|
7
|
+
MISSING_TARGET_LABEL = 'no-consumers'
|
8
|
+
|
9
|
+
def initialize(queue_name:, routing_key: nil, consumer_tag: nil)
|
10
|
+
@queue_name = queue_name
|
11
|
+
@routing_fragments = routing_key.to_s.split('.')
|
12
|
+
@consumer_tag = consumer_tag
|
13
|
+
|
14
|
+
@source_app = routing_fragments[0] || MISSING_SOURCE_LABEL
|
15
|
+
@entity = routing_fragments[1] || ''
|
16
|
+
@actions = routing_fragments[2..-1] || []
|
17
|
+
@target_app = consumer_tag_to_application_name(consumer_tag) || MISSING_TARGET_LABEL
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :source_app, :target_app, :entity, :actions, :queue_name
|
21
|
+
|
22
|
+
def missing_source?
|
23
|
+
source_app == MISSING_SOURCE_LABEL
|
24
|
+
end
|
25
|
+
|
26
|
+
def missing_target?
|
27
|
+
target_app == MISSING_TARGET_LABEL
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_consumer_tag?
|
31
|
+
target_app == DEFAULT_CONSUMER_TAG
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
{ queue_name: queue_name, routing_key: routing_fragments.join('.'), consumer_tag: consumer_tag }
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
eql?(other)
|
40
|
+
end
|
41
|
+
|
42
|
+
def eql?(other)
|
43
|
+
[queue_name, routing_fragments, consumer_tag] == [other.queue_name, other.routing_fragments, other.consumer_tag]
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
attr_reader :routing_fragments, :consumer_tag
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def consumer_tag_to_application_name(tag)
|
53
|
+
return DEFAULT_CONSUMER_TAG if tag =~ /^bunny-/ || tag =~ /^hutch-/ || tag =~ /^amq\.ctag/
|
54
|
+
return tag.split('-')[0..-6].join('-') if tag =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
55
|
+
return tag.split('-')[0..-3].join('-') if tag =~ /[0-9]+-[0-9]+$/
|
56
|
+
tag
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'rabbitmq-graph'
|
5
|
+
s.version = '0.1.1'
|
6
|
+
s.summary = 'Discover RabbitMQ topology'
|
7
|
+
s.description = 'Map out RabbitMQ topology with the use of routing key conventions and consumer tags.'
|
8
|
+
s.authors = ['David Lantos']
|
9
|
+
s.email = 'david.lantos+rabbitmq-graph@gmail.com'
|
10
|
+
s.bindir = 'bin'
|
11
|
+
s.executables = ['rabbitmq-graph']
|
12
|
+
s.files = `git ls-files lib bin *.gemspec *.md`.split
|
13
|
+
s.homepage = 'https://github.com/sldblog/rabbitmq-graph'
|
14
|
+
s.metadata = { 'source_code_uri' => 'https://github.com/sldblog/rabbitmq-graph' }
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'hutch', '~> 0.24'
|
17
|
+
s.add_runtime_dependency 'ruby-progressbar', '~> 1.9'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rabbitmq-graph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Lantos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hutch
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.24'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.24'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-progressbar
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
description: Map out RabbitMQ topology with the use of routing key conventions and
|
42
|
+
consumer tags.
|
43
|
+
email: david.lantos+rabbitmq-graph@gmail.com
|
44
|
+
executables:
|
45
|
+
- rabbitmq-graph
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- README.md
|
50
|
+
- bin/rabbitmq-graph
|
51
|
+
- lib/rabbitmq-graph.rb
|
52
|
+
- lib/rabbitmq-graph/discover.rb
|
53
|
+
- lib/rabbitmq-graph/dot_format.rb
|
54
|
+
- lib/rabbitmq-graph/markdown_table_format.rb
|
55
|
+
- lib/rabbitmq-graph/route.rb
|
56
|
+
- rabbitmq-graph.gemspec
|
57
|
+
homepage: https://github.com/sldblog/rabbitmq-graph
|
58
|
+
licenses: []
|
59
|
+
metadata:
|
60
|
+
source_code_uri: https://github.com/sldblog/rabbitmq-graph
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 2.7.6
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Discover RabbitMQ topology
|
81
|
+
test_files: []
|