rabbitmq-graph 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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: []