rabbitmq-graph 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []