├── Gemfile
├── .gitignore
├── .travis.yml
├── Rakefile
├── test
├── helper.rb
└── plugin
│ ├── test_out_rabbitmq.rb
│ └── test_in_rabbitmq.rb
├── COPYING
├── fluent-plugin-rabbitmq.gemspec
├── README.md
├── lib
└── fluent
│ └── plugin
│ ├── out_rabbitmq.rb
│ └── in_rabbitmq.rb
└── LICENSE.txt
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | .gem
11 | *.gem
12 | .idea
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.6
4 | cache: bundler
5 | dist: xenial
6 | sudo: true
7 | services:
8 | - rabbitmq
9 | addons:
10 | apt:
11 | packages:
12 | - rabbitmq-server
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new(:test) do |test|
5 | test.libs << "test"
6 | test.pattern = "test/**/test_*.rb"
7 | test.verbose = false
8 | test.warning = false
9 | end
10 |
11 | task :default => :test
12 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require "test-unit"
2 | require "serverengine"
3 | require "fluent/test"
4 |
5 | require "bunny"
6 | require "json"
7 | require "ltsv"
8 | require "msgpack"
9 |
10 | module ServerEngine
11 | def windows? #XXX: workaround
12 | false
13 | end
14 | end
15 |
16 | def config_element(name = 'test', argument = '', params = {}, elements = [])
17 | Fluent::Config::Element.new(name, argument, params, elements)
18 | end
19 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright 2017 NTT Communications
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/fluent-plugin-rabbitmq.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path("../lib", __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 |
4 | Gem::Specification.new do |spec|
5 | spec.name = "fluent-plugin-rabbitmq"
6 | spec.version = "0.1.5"
7 | spec.authors = ["NTT DOCOMO BUSINESS"]
8 | spec.email = ["masaki.matsushita@ntt.com"]
9 |
10 | spec.summary = %q{fluent plugin for rabbitmq (AMQP)}
11 | spec.description = %q{fluent plugin for rabbitmq (AMQP)}
12 | spec.homepage = "https://github.com/nttcom/fluent-plugin-rabbitmq"
13 | spec.license = "Apache-2.0"
14 |
15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16 | spec.require_paths = ["lib"]
17 |
18 | spec.required_ruby_version = ">= 2.5"
19 |
20 | spec.add_development_dependency "bundler"
21 | spec.add_development_dependency "rake"
22 | spec.add_development_dependency "test-unit"
23 | spec.add_development_dependency "ltsv"
24 | spec.add_development_dependency "msgpack"
25 |
26 | spec.add_runtime_dependency "fluentd", ">= 1.0.0"
27 |
28 | spec.add_dependency "bunny", "~> 2.14.4"
29 | end
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fluent-plugin-rabbitmq
2 |
3 | ## Overview
4 |
5 | This repository includes input/output plugins for RabbitMQ.
6 |
7 | ## Requirements
8 |
9 | fluentd >= 0.14.0
10 |
11 | ## Installation
12 |
13 | $ fluent-gem install fluent-plugin-rabbitmq
14 |
15 | ## Testing
16 |
17 | $ rabbitmq-server
18 | $ rake test
19 |
20 | ## Configuration
21 |
22 | ### Input
23 |
24 | ```
25 |
26 | @type rabbitmq
27 | tag foo
28 | host 127.0.0.1
29 | # or hosts ["192.168.1.1", "192.168.1.2"]
30 | user guest
31 | pass guest
32 | vhost /
33 | exchange foo # not required. if specified, the queue will be bound to the exchange
34 | queue bar
35 | routing_key hoge # if not specified, the tag is used
36 | heartbeat 10 # integer as seconds or :server (interval specified by server)
37 |
38 | @type json # or msgpack, ltsv, none
39 |
40 |
41 | ```
42 |
43 | #### Other Configurations for Input
44 |
45 | |key|example|default value|description|
46 | |:--|---|---|---|
47 | |durable|true|false|set durable flag of the queue|
48 | |exclusive|true|false|set exclusive flag of the queue|
49 | |auto_delete|true|false|set auto_delete flag of the queue|
50 | |ttl|60000|nil|queue ttl in ms|
51 | |prefetch_count|10|nil||
52 | |consumer_pool_size|5|nil||
53 | |include_headers|true|false|include headers in events|
54 | |headers_key|string|header|key name of headers|
55 | |create_exchange|true|false|create exchange or not|
56 | |exchange_to_bind|string|nil|exchange to bind created exchange|
57 | |exchange_type|direct|topic|type of created exchange|
58 | |exchange_routing_key|hoge|nil|created exchange routing key|
59 | |exchange_durable|true|false|durability of create exchange|
60 | |exchange_no_declare|true|false|passive declare option for exchange|
61 | |manual_ack|true|false|manual ACK|
62 | |queue_mode|"lazy"|nil|queue mode|
63 | |queue_type|"quorum"|nil|queue type|
64 |
65 |
66 | ### Output
67 |
68 | ```
69 |
70 | @type rabbitmq
71 | host 127.0.0.1
72 | # or hosts ["192.168.1.1", "192.168.1.2"]
73 | user guest
74 | pass guest
75 | vhost /
76 | format json # or msgpack, ltsv, none
77 | exchange foo # required: name of exchange
78 | exchange_type fanout # required: type of exchange e.g. topic, direct
79 | exchange_durable false
80 | routing_key hoge # if not specified, the tag is used
81 | heartbeat 10 # integer as seconds or :server (interval specified by server)
82 |
83 | @type json # or msgpack, ltsv, none
84 |
85 | # to use in buffered mode
86 |
87 |
88 | ```
89 |
90 | #### Other Configurations for Output
91 |
92 | |key|example|default value|description|
93 | |:--|---|---|---|
94 | |persistent|true|false|messages is persistent to disk|
95 | |timestamp|true|false|if true, time of record is used as timestamp in AMQP message|
96 | |content_type|application/json|nil|message content type|
97 | |frame_max|131072|nil|maximum permissible size of a frame|
98 | |mandatory||true|nil||
99 | |expiration|3600|nil|message time-to-live|
100 | |message_type||nil||
101 | |priority||nil||
102 | |app_id||nil||
103 | |id_key|message_id|nil|id to specify message_id|
104 |
105 | ### TLS related configurations
106 |
107 | ```
108 | tls false # enable TLS or not
109 | tls_cert /path/to/cert
110 | tls_key /path/to/key
111 | tls_ca_certificates ["/path/to/ca_certificate"]
112 | verify_peer true
113 | ```
114 |
115 | ### Other Configurations for Input/Output
116 |
117 | |key|example|default value|description|
118 | |:--|---|---|---|
119 | |automatically_recover|true|nil|automatic network failure recovery|
120 | |network_recovery_interval|30|nil|interval between reconnection attempts|
121 | |recovery_attempts|3|nil|limits the number of connection recovery|
122 | |connection_timeout|30|nil||
123 | |continuation_timeout|600|nil||
124 |
125 | ## License
126 |
127 | The gem is available as open source under the terms of the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt).
128 |
129 |
--------------------------------------------------------------------------------
/test/plugin/test_out_rabbitmq.rb:
--------------------------------------------------------------------------------
1 | require_relative "../helper"
2 |
3 | require "fluent/test/driver/output"
4 | require "fluent/plugin/out_rabbitmq"
5 |
6 | class RabbitMQOutputTest < Test::Unit::TestCase
7 | def setup
8 | Fluent::Test.setup
9 | @time = Fluent::Engine.now
10 | @tag = "test.test"
11 |
12 | bunny = Bunny.new
13 | bunny.start
14 | @channel = bunny.create_channel
15 | @queue = @channel.queue("test_out_fanout")
16 | fanout_exchange = Bunny::Exchange.new(@channel, "fanout", "test_out_fanout")
17 | @queue.bind(fanout_exchange)
18 | end
19 |
20 | def teardown
21 | super
22 | Fluent::Engine.stop
23 | end
24 |
25 | CONFIG = %[
26 | host localhost
27 | port 5672
28 | user guest
29 | pass guest
30 | vhost /
31 | exchange test_out_fanout
32 | exchange_type fanout
33 | heartbeat 10
34 | ]
35 |
36 | def create_driver(conf = CONFIG)
37 | Fluent::Test::Driver::Output.new(Fluent::Plugin::RabbitMQOutput).configure(conf)
38 | end
39 |
40 | def test_configure
41 | d = create_driver
42 | assert_equal "localhost", d.instance.host
43 | assert_equal 5672, d.instance.port
44 | assert_equal "guest", d.instance.user
45 | assert_equal "guest", d.instance.pass
46 | assert_equal "/", d.instance.vhost
47 | assert_equal "test_out_fanout", d.instance.exchange
48 | assert_equal "fanout", d.instance.exchange_type
49 | assert_equal 10, d.instance.heartbeat
50 | end
51 |
52 | def test_start_and_shutdown
53 | d = create_driver
54 |
55 | d.instance.start
56 | d.instance.shutdown
57 | end
58 |
59 | def test_emit
60 | d = create_driver
61 |
62 | record = {"test_emit" => 1}
63 | d.run(default_tag: "test.test") do
64 | d.feed(@time, record)
65 | end
66 |
67 | _, _, body = @queue.pop
68 | assert_equal(record, JSON.parse(body))
69 | end
70 |
71 | def test_topic
72 | d = create_driver(%[
73 | exchange test_out_topic
74 | exchange_type topic
75 | routing_key test_out_topic
76 | ])
77 |
78 | queue = @channel.queue("test_out_topic")
79 | topic_exchange = Bunny::Exchange.new(@channel, "topic", "test_out_topic")
80 | queue.bind(topic_exchange, routing_key: "test_out_topic")
81 |
82 | record = {"test_topic" => 1}
83 | d.run(default_tag: "test.test") do
84 | d.feed(@time, record)
85 | end
86 |
87 | _, _, body = queue.pop
88 | assert_equal(record, JSON.parse(body))
89 | end
90 |
91 | def test_timestamp
92 | d = create_driver(%[
93 | exchange test_out_fanout
94 | exchange_type fanout
95 | timestamp true
96 | ])
97 |
98 | record = {"test_timestamp" => true}
99 | d.run(default_tag: "test.test") do
100 | d.feed(@time, record)
101 | end
102 |
103 | _, properties, _ = @queue.pop
104 | assert_equal(@time, properties[:timestamp].to_i)
105 | end
106 |
107 | def test_id_key
108 | d = create_driver(%[
109 | exchange test_out_fanout
110 | exchange_type fanout
111 | id_key test_id
112 | ])
113 |
114 | message_id = "abc123"
115 | record = {"test_id" => message_id, "foo" => "bar"}
116 | d.run(default_tag: "test.test") do
117 | d.feed(@time, record)
118 | end
119 |
120 | _, properties, _ = @queue.pop
121 | assert_equal(message_id, properties[:message_id])
122 | end
123 |
124 | def test_server_interval
125 | d = create_driver(%[
126 | exchange test
127 | exchange_type fanout
128 | heartbeat server
129 | ])
130 | assert_equal(:server, d.instance.heartbeat)
131 | end
132 |
133 | def test_server_interval_invalid_string
134 | assert_raise ArgumentError do
135 | create_driver(%[
136 | exchange test
137 | exchange_type fanout
138 | heartbeat invalid
139 | ])
140 | end
141 | end
142 |
143 | def test_emit_ltsv
144 | d = create_driver(%[
145 | exchange test_out_fanout
146 | exchange_type fanout
147 | format ltsv
148 | ])
149 |
150 | record = {test_emit_ltsv: "2"}
151 | d.run(default_tag: "test.test") do
152 | d.feed(@time, record)
153 | end
154 |
155 | _, _, body = @queue.pop
156 | assert_equal(record, LTSV.parse(body).first)
157 | end
158 |
159 | def test_emit_msgpack
160 | d = create_driver(%[
161 | exchange test_out_fanout
162 | exchange_type fanout
163 | format msgpack
164 | ])
165 |
166 | record = {"test_emit_msgpack" => true}
167 | d.run(default_tag: "test.test") do
168 | d.feed(@time, record)
169 | end
170 |
171 | _, _, body = @queue.pop
172 | assert_equal(record, MessagePack.unpack(body))
173 | end
174 |
175 | def test_emit_single_value
176 | d = create_driver(%[
177 | exchange test_out_fanout
178 | exchange_type fanout
179 | format single_value
180 | ])
181 |
182 | string = "test_emit_single_value"
183 | record = {"message" => string}
184 | d.run(default_tag: "test.test") do
185 | d.feed(@time, record)
186 | end
187 |
188 | _, _, body = @queue.pop
189 | body.force_encoding("utf-8")
190 | assert_equal(string, body.chomp)
191 | end
192 |
193 | def test_buffered_emit
194 | d = create_driver(%[
195 | exchange test_out_fanout
196 | exchange_type fanout
197 | format json
198 | timestamp true
199 |
200 |
201 | ])
202 |
203 | record = {"test_emit" => 1}
204 | d.run(default_tag: "test.test") do
205 | d.feed(@time, record)
206 | end
207 |
208 | _, properties, body = @queue.pop
209 | assert_equal(record, JSON.parse(body))
210 | assert_equal(@time, properties[:timestamp].to_i)
211 | end
212 | end
213 |
--------------------------------------------------------------------------------
/lib/fluent/plugin/out_rabbitmq.rb:
--------------------------------------------------------------------------------
1 | #
2 | # fluent-plugin-rabbitmq
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 | #
17 | require "fluent/plugin/output"
18 |
19 | module Fluent::Plugin
20 | class RabbitMQOutput < Output
21 | Fluent::Plugin.register_output("rabbitmq", self)
22 |
23 | helpers :formatter, :inject, :compat_parameters
24 |
25 | config_section :format do
26 | config_set_default :@type, "json"
27 | end
28 |
29 | config_param :host, :string, default: nil
30 | config_param :hosts, :array, default: nil
31 | config_param :port, :integer, default: nil
32 | config_param :user, :string, default: nil
33 | config_param :pass, :string, default: nil, secret: true
34 | config_param :vhost, :string, default: nil
35 | config_param :connection_timeout, :time, default: nil
36 | config_param :continuation_timeout, :integer, default: nil
37 | config_param :automatically_recover, :bool, default: nil
38 | config_param :network_recovery_interval, :time, default: nil
39 | config_param :recovery_attempts, :integer, default: nil
40 | config_param :auth_mechanism, :string, default: nil
41 | config_param :heartbeat, default: nil do |param|
42 | param == "server" ? :server : Integer(param)
43 | end
44 | config_param :frame_max, :integer, default: nil
45 |
46 | config_param :tls, :bool, default: false
47 | config_param :tls_cert, :string, default: nil
48 | config_param :tls_key, :string, default: nil
49 | config_param :tls_ca_certificates, :array, default: nil
50 | config_param :verify_peer, :bool, default: true
51 |
52 | config_param :exchange, :string
53 | config_param :exchange_type, :string
54 | config_param :exchange_durable, :bool, default: false
55 | config_param :exchange_no_declare, :bool, default: false
56 |
57 | config_param :persistent, :bool, default: false
58 | config_param :routing_key, :string, default: nil
59 | config_param :id_key, :string, default: nil
60 | config_param :timestamp, :bool, default: false
61 | config_param :content_type, :string, default: nil
62 | config_param :content_encoding, :string, default: nil
63 | config_param :expiration, :integer, default: nil
64 | config_param :message_type, :string, default: nil
65 | config_param :priority, :integer, default: nil
66 | config_param :app_id, :string, default: nil
67 |
68 | def initialize
69 | super
70 | require "bunny"
71 | end
72 |
73 | def configure(conf)
74 | compat_parameters_convert(conf, :inject, :formatter, default_chunk_key: "time")
75 |
76 | super
77 |
78 | bunny_options = {}
79 | bunny_options[:host] = @host if @host
80 | bunny_options[:hosts] = @hosts if @hosts
81 | bunny_options[:port] = @port if @port
82 | bunny_options[:user] = @user if @user
83 | bunny_options[:pass] = @pass if @pass
84 | bunny_options[:vhost] = @vhost if @vhost
85 | bunny_options[:connection_timeout] = @connection_timeout if @connection_timeout
86 | bunny_options[:continuation_timeout] = @continuation_timeout if @continuation_timeout
87 | bunny_options[:automatically_recover] = @automatically_recover if @automatically_recover
88 | bunny_options[:network_recovery_interval] = @network_recovery_interval if @network_recovery_interval
89 | bunny_options[:recovery_attempts] = @recovery_attempts
90 | bunny_options[:auth_mechanism] = @auth_mechanism if @auth_mechanism
91 | bunny_options[:heartbeat] = @heartbeat if @heartbeat
92 | bunny_options[:frame_max] = @frame_max if @frame_max
93 |
94 | bunny_options[:tls] = @tls
95 | bunny_options[:tls_cert] = @tls_cert if @tls_cert
96 | bunny_options[:tls_key] = @tls_key if @tls_key
97 | bunny_options[:tls_ca_certificates] = @tls_ca_certificates if @tls_ca_certificates
98 | bunny_options[:verify_peer] = @verify_peer
99 |
100 | @bunny = Bunny.new(bunny_options)
101 |
102 | @publish_options = {}
103 | @publish_options[:content_type] = @content_type if @content_type
104 | @publish_options[:content_encoding] = @content_encoding if @content_encoding
105 | @publish_options[:persistent] = @persistent if @persistent
106 | @publish_options[:mandatory] = @mandatory if @mandatory
107 | @publish_options[:expiration] = @expiration if @expiration
108 | @publish_options[:type] = @message_type if @message_type
109 | @publish_options[:priority] = @priority if @priority
110 | @publish_options[:app_id] = @app_id if @app_id
111 |
112 | @formatter = formatter_create(default_type: @type)
113 | end
114 |
115 | def multi_workers_ready?
116 | true
117 | end
118 |
119 | def prefer_buffered_processing
120 | false
121 | end
122 |
123 | def start
124 | super
125 | @bunny.start
126 | @channel = @bunny.create_channel
127 | exchange_options = {
128 | durable: @exchange_durable,
129 | auto_delete: @exchange_auto_delete,
130 | no_declare: @exchange_no_declare
131 | }
132 | @bunny_exchange = Bunny::Exchange.new(@channel, @exchange_type, @exchange, exchange_options)
133 | end
134 |
135 | def shutdown
136 | @bunny.close
137 | super
138 | end
139 |
140 | def set_publish_options(tag, time, record)
141 | @publish_options[:timestamp] = time.to_i if @timestamp
142 |
143 | if @exchange_type != "fanout"
144 | @publish_options[:routing_key] = @routing_key || tag
145 | end
146 |
147 | if @id_key
148 | id = record[@id_key]
149 | @publish_options[:message_id] = id if id
150 | end
151 | end
152 |
153 | def process(tag, es)
154 | es.each do |time, record|
155 | set_publish_options(tag, time, record)
156 | record = inject_values_to_record(tag, time, record)
157 | buf = @formatter.format(tag, time, record)
158 | @bunny_exchange.publish(buf, @publish_options)
159 | end
160 | end
161 |
162 | def write(chunk)
163 | tag = chunk.metadata.tag
164 |
165 | chunk.each do |time, record|
166 | set_publish_options(tag, time, record)
167 | record = inject_values_to_record(tag, time, record)
168 | buf = @formatter.format(tag, time, record)
169 | @bunny_exchange.publish(buf, @publish_options)
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/lib/fluent/plugin/in_rabbitmq.rb:
--------------------------------------------------------------------------------
1 | #
2 | # fluent-plugin-rabbitmq
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 | #
17 | require "fluent/plugin/input"
18 |
19 | module Fluent::Plugin
20 | class RabbitMQInput < Input
21 | Fluent::Plugin.register_input("rabbitmq", self)
22 |
23 | helpers :parser, :compat_parameters
24 |
25 | config_param :tag, :string
26 |
27 | config_section :parse do
28 | config_set_default :@type, "json"
29 | end
30 |
31 | config_param :host, :string, default: nil
32 | config_param :hosts, :array, default: nil
33 | config_param :port, :integer, default: nil
34 | config_param :user, :string, default: nil
35 | config_param :pass, :string, default: nil, secret: true
36 | config_param :vhost, :string, default: nil
37 |
38 | config_param :routing_key, :string, default: nil
39 | config_param :connection_timeout, :time, default: nil
40 | config_param :continuation_timeout, :integer, default: nil
41 | config_param :automatically_recover, :bool, default: nil
42 | config_param :network_recovery_interval, :time, default: nil
43 | config_param :recovery_attempts, :integer, default: nil
44 | config_param :auth_mechanism, :string, default: nil
45 | config_param :heartbeat, default: nil do |param|
46 | param == "server" ? :server : Integer(param)
47 | end
48 | config_param :consumer_pool_size, :integer, default: nil
49 |
50 | config_param :exchange, :string, default: nil
51 | config_param :create_exchange, :bool, default: false
52 | config_param :exchange_to_bind, :string, default: nil
53 | config_param :exchange_type, :string, default: "topic"
54 | config_param :exchange_routing_key, :string, default: nil
55 | config_param :exchange_durable, :bool, default: false
56 |
57 | config_param :tls, :bool, default: false
58 | config_param :tls_cert, :string, default: nil
59 | config_param :tls_key, :string, default: nil
60 | config_param :tls_ca_certificates, :array, default: nil
61 | config_param :verify_peer, :bool, default: true
62 |
63 | config_param :queue, :string
64 | config_param :durable, :bool, default: false
65 | config_param :exclusive, :bool, default: false
66 | config_param :auto_delete, :bool, default: false
67 | config_param :prefetch_count, :integer, default: nil
68 | config_param :ttl, :integer, default: nil
69 |
70 | config_param :include_headers, :bool, default: false
71 | config_param :include_delivery_info, :bool, default: false
72 | config_param :headers_key, :string, default: "headers"
73 | config_param :delivery_info_key, :string, default: "delivery_info"
74 | config_param :manual_ack, :bool, default: false
75 | config_param :queue_mode, :string, default: nil
76 | config_param :queue_type, :string, default: nil
77 |
78 | def initialize
79 | super
80 | require "bunny"
81 | end
82 |
83 | def configure(conf)
84 | compat_parameters_convert(conf, :parser)
85 |
86 | super
87 |
88 | bunny_options = {}
89 | bunny_options[:host] = @host if @host
90 | bunny_options[:hosts] = @hosts if @hosts
91 | bunny_options[:port] = @port if @port
92 | bunny_options[:user] = @user if @user
93 | bunny_options[:pass] = @pass if @pass
94 | bunny_options[:vhost] = @vhost if @vhost
95 | bunny_options[:connection_timeout] = @connection_timeout if @connection_timeout
96 | bunny_options[:continuation_timeout] = @continuation_timeout if @continuation_timeout
97 | bunny_options[:automatically_recover] = @automatically_recover if @automatically_recover
98 | bunny_options[:network_recovery_interval] = @network_recovery_interval if @network_recovery_interval
99 | bunny_options[:recovery_attempts] = @recovery_attempts
100 | bunny_options[:auth_mechanism] = @auth_mechanism if @auth_mechanism
101 | bunny_options[:heartbeat] = @heartbeat if @heartbeat
102 |
103 | bunny_options[:tls] = @tls
104 | bunny_options[:tls_cert] = @tls_cert if @tls_cert
105 | bunny_options[:tls_key] = @tls_key if @tls_key
106 | bunny_options[:tls_ca_certificates] = @tls_ca_certificates if @tls_ca_certificates
107 | bunny_options[:verify_peer] = @verify_peer
108 |
109 | @parser = parser_create
110 |
111 | @routing_key ||= @tag
112 | @bunny = Bunny.new(bunny_options)
113 | end
114 |
115 | def start
116 | super
117 | @bunny.start
118 | channel = @bunny.create_channel(nil, @consumer_pool_size)
119 | channel.prefetch(@prefetch_count) if @prefetch_count
120 | if @create_exchange
121 | exchange_options = {
122 | durable: @exchange_durable,
123 | auto_delete: @auto_delete
124 | }
125 | @bunny_exchange = Bunny::Exchange.new(channel, @exchange_type, @exchange, exchange_options)
126 | if @exchange_to_bind
127 | @bunny_exchange.bind(@exchange_to_bind, routing_key: @exchange_routing_key)
128 | end
129 | end
130 | queue_arguments = {}
131 | queue_arguments["x-message-ttl"] = @ttl if @ttl
132 | queue_arguments["x-queue-mode"] = @queue_mode if @queue_mode
133 | queue_arguments["x-queue-type"] = @queue_type if @queue_type
134 | queue = channel.queue(
135 | @queue,
136 | durable: @durable,
137 | exclusive: @exclusive,
138 | auto_delete: @auto_delete,
139 | arguments: queue_arguments
140 | )
141 | if @exchange
142 | queue.bind(@exchange, routing_key: @routing_key)
143 | end
144 | queue.subscribe(manual_ack: @manual_ack) do |delivery_info, properties, payload|
145 | begin
146 | @parser.parse(payload) do |time, record|
147 | time = if properties[:timestamp]
148 | Fluent::EventTime.from_time(properties[:timestamp])
149 | else
150 | time
151 | end
152 | record ||= {}
153 | if @include_headers
154 | record[@headers_key] = properties.headers
155 | end
156 | if @include_delivery_info
157 | record[@delivery_info_key] = delivery_info
158 | end
159 | router.emit(@tag, time, record)
160 | end
161 | rescue Fluent::Plugin::Parser::ParserError => e
162 | log.error "Parser error: #{e.message}", error: e, payload: payload, tag: @tag
163 | ensure
164 | channel.ack(delivery_info.delivery_tag) if @manual_ack
165 | end
166 | end
167 | end
168 |
169 | def multi_workers_ready?
170 | true
171 | end
172 |
173 | def shutdown
174 | @bunny.close
175 | super
176 | end
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 |
--------------------------------------------------------------------------------
/test/plugin/test_in_rabbitmq.rb:
--------------------------------------------------------------------------------
1 | require_relative "../helper"
2 |
3 | require "fluent/test/driver/input"
4 | require "fluent/plugin/in_rabbitmq"
5 |
6 | class RabbitMQInputTest < Test::Unit::TestCase
7 | def setup
8 | Fluent::Test.setup
9 | @time = Fluent::Engine.now
10 |
11 | bunny = Bunny.new
12 | bunny.start
13 | @channel = bunny.create_channel
14 | queue = @channel.queue("test_in_fanout")
15 | @fanout_exchange = Bunny::Exchange.new(@channel, :fanout, "test_in_fanout")
16 | @fanout_exchange_bind = Bunny::Exchange.new(@channel, :fanout, "test_in_bind")
17 | @topic_exchange = Bunny::Exchange.new(@channel, :topic, "test_in_topic")
18 | queue.bind(@fanout_exchange)
19 | end
20 |
21 | def teardown
22 | super
23 | Fluent::Engine.stop
24 | end
25 |
26 | CONFIG = %[
27 | tag test.test
28 | host localhost
29 | port 5672
30 | user guest
31 | pass guest
32 | vhost /
33 | heartbeat 10
34 | queue test_in_fanout
35 | ]
36 |
37 | def create_driver(conf = CONFIG)
38 | Fluent::Test::Driver::Input.new(Fluent::Plugin::RabbitMQInput).configure(conf)
39 | end
40 |
41 | def test_configure
42 | d = create_driver
43 | assert_equal "localhost", d.instance.host
44 | assert_equal 5672, d.instance.port
45 | assert_equal "guest", d.instance.user
46 | assert_equal "guest", d.instance.pass
47 | assert_equal "/", d.instance.vhost
48 | assert_equal 10, d.instance.heartbeat
49 | end
50 |
51 | def test_start_and_shutdown
52 | d = create_driver
53 |
54 | d.instance.start
55 | d.instance.shutdown
56 | end
57 |
58 | def test_emit
59 | d = create_driver
60 |
61 | expect_hash = {"foo" => "bar"}
62 | d.run(expect_emits: 1) do
63 | @fanout_exchange.publish(expect_hash.to_json)
64 | end
65 |
66 | d.events.each do |event|
67 | assert_equal expect_hash, event[2]
68 | end
69 | end
70 |
71 | def test_emit_ltsv
72 | conf = CONFIG.clone
73 | conf << "\nformat ltsv\n"
74 | d = create_driver(conf)
75 |
76 | expect_hash = {"foo" => "bar", "bar" => "baz"}
77 | d.run(expect_emits: 1) do
78 | @fanout_exchange.publish(LTSV.dump(expect_hash))
79 | end
80 |
81 | d.events.each do |event|
82 | assert_equal expect_hash, event[2]
83 | end
84 | end
85 |
86 | def test_emit_msgpack
87 | conf = CONFIG.clone
88 | conf << "\nformat msgpack\n"
89 | d = create_driver(conf)
90 |
91 | expect_hash = {"foo" => "bar", "bar" => "baz"}
92 | d.run(expect_emits: 1) do
93 | @fanout_exchange.publish(expect_hash.to_msgpack)
94 | end
95 |
96 | d.events.each do |event|
97 | assert_equal expect_hash, event[2]
98 | end
99 | end
100 |
101 | def test_emit_single_value
102 | conf = CONFIG.clone
103 | conf << "\nformat none\n"
104 | d = create_driver(conf)
105 |
106 | expect_string = "foo"
107 | d.run(expect_emits: 1) do
108 | @fanout_exchange.publish(expect_string)
109 | end
110 |
111 | d.events.each do |event|
112 | assert_equal({"message" => expect_string}, event[2])
113 | end
114 | end
115 |
116 | def test_emit_with_timestamp
117 | d = create_driver
118 |
119 | expect_time = Fluent::EventTime.parse("2018-08-15 13:14:15 UTC")
120 | expect_hash = {"foo" => "bar"}
121 | d.run(expect_emits: 1) do
122 | @fanout_exchange.publish(expect_hash.to_json, timestamp: expect_time.to_i)
123 | end
124 |
125 | d.events.each do |event|
126 | assert_equal expect_time, event[1]
127 | assert_equal expect_hash, event[2]
128 | end
129 | end
130 |
131 | def test_emit_direct
132 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_direct")
133 | d = create_driver(conf)
134 |
135 | queue = @channel.queue("test_in_direct")
136 | direct_exchange = Bunny::Exchange.new(@channel, "direct", "test_in_direct")
137 | queue.bind(direct_exchange)
138 |
139 | expect_hash = {"foo" => "bar"}
140 | d.run(expect_emits: 1) do
141 | direct_exchange.publish(expect_hash.to_json)
142 | end
143 |
144 | d.events.each do |event|
145 | assert_equal expect_hash, event[2]
146 | end
147 | end
148 |
149 | def test_emit_topic
150 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_topic")
151 | d = create_driver(conf)
152 |
153 | queue = @channel.queue("test_in_topic")
154 | topic_exchange = Bunny::Exchange.new(@channel, "topic", "test_in_topic")
155 | queue.bind(topic_exchange, routing_key: "test_in_topic")
156 |
157 | expect_hash = {"foo" => "bar"}
158 | d.run(expect_emits: 1) do
159 | topic_exchange.publish(expect_hash.to_json, routing_key: "test_in_topic")
160 | end
161 |
162 | d.events.each do |event|
163 | assert_equal expect_hash, event[2]
164 | end
165 | end
166 |
167 | def test_durable
168 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_durable")
169 | conf << "\ndurable true\n"
170 | d = create_driver(conf)
171 | d.run {}
172 | assert_nothing_raised do
173 | @channel.queue("test_in_durable", durable: true)
174 | end
175 | end
176 |
177 | def test_auto_delete
178 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_auto_delete")
179 | conf << "\nauto_delete true\n"
180 | d = create_driver(conf)
181 | d.run {}
182 | assert_nothing_raised do
183 | @channel.queue("test_in_auto_delete", auto_delete: true)
184 | end
185 | end
186 |
187 | def test_exclusive
188 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_exclusive")
189 | conf << "\nexclusive true\n"
190 | d = create_driver(conf)
191 | d.run {}
192 | assert_nothing_raised do
193 | @channel.queue("test_in_exclusive", exclusive: true)
194 | end
195 | end
196 |
197 | def test_hosts
198 | conf = CONFIG.clone.gsub(/host\slocalhost/, "hosts [\"localhost\"]")
199 | d = create_driver(conf)
200 | d.run {}
201 | end
202 |
203 | def test_prefetch_count
204 | conf = CONFIG.clone
205 | conf << "\nprefetch_count 16^n"
206 | d = create_driver(conf)
207 | d.run {}
208 | end
209 |
210 | def test_bind
211 | conf = CONFIG.clone
212 | conf.gsub!(/queue\stest_in_fanout/, "queue test_in_bind")
213 | conf << "\nexchange test_in_bind"
214 | d = create_driver(conf)
215 |
216 | expect_hash = {"foo" => "bar", "bar" => "baz"}
217 | d.run(expect_emits: 1) do
218 | @fanout_exchange_bind.publish(expect_hash.to_json)
219 | end
220 |
221 | d.events.each do |event|
222 | assert_equal expect_hash, event[2]
223 | end
224 | end
225 |
226 | def test_bind_routing_by_tag
227 | conf = CONFIG.clone
228 | conf.gsub!(/queue\stest_in_topic/, "queue test_in_bind_routing")
229 | conf << "\nexchange test_in_topic"
230 | d = create_driver(conf)
231 |
232 | expect_hash = {"foo" => "bar", "bar" => "baz"}
233 | d.run(expect_emits: 1) do
234 | @topic_exchange.publish(expect_hash.to_json, routing_key: "test.test")
235 | end
236 |
237 | d.events.each do |event|
238 | assert_equal expect_hash, event[2]
239 | end
240 | end
241 |
242 | def test_bind_routing_by_key
243 | conf = CONFIG.clone
244 | conf.gsub!(/queue\stest_in_topic/, "queue test_in_bind_routing")
245 | conf << "\nexchange test_in_topic\nrouting_key test.bind.routing"
246 | d = create_driver(conf)
247 |
248 | expect_hash = {"foo" => "bar", "bar" => "baz"}
249 | d.run(expect_emits: 1) do
250 | @topic_exchange.publish(expect_hash.to_json, routing_key: "test.bind.routing")
251 | end
252 |
253 | d.events.each do |event|
254 | assert_equal expect_hash, event[2]
255 | end
256 | end
257 |
258 | def test_include_headers
259 | conf = CONFIG.clone
260 | conf << "\ninclude_headers true\n"
261 | d = create_driver(conf)
262 |
263 | hash = {"foo" => "bar"}
264 | headers = {"hoge" => "fuga"}
265 | expect_hash = hash.dup
266 | expect_hash["headers"] = headers
267 |
268 | d.run(expect_emits: 1) do
269 | @fanout_exchange.publish(hash.to_json, headers: headers)
270 | end
271 |
272 | d.events.each do |event|
273 | assert_equal expect_hash, event[2]
274 | end
275 | end
276 |
277 | def test_include_headers_without_payload
278 | conf = CONFIG.clone
279 | conf << "\ninclude_headers true\n"
280 | d = create_driver(conf)
281 |
282 | headers = {"hoge" => "fuga"}
283 | expect_hash = {"headers" => headers}
284 |
285 | d.run(expect_emits: 1) do
286 | @fanout_exchange.publish("", headers: headers)
287 | end
288 |
289 | d.events.each do |event|
290 | assert_equal expect_hash, event[2]
291 | end
292 | end
293 |
294 | def test_headers_key
295 | conf = CONFIG.clone
296 | conf << "\ninclude_headers true\nheaders_key test"
297 | d = create_driver(conf)
298 |
299 | hash = {"foo" => "bar"}
300 | headers = {"hoge" => "fuga"}
301 | expect_hash = hash.dup
302 | expect_hash["test"] = headers
303 |
304 | d.run(expect_emits: 1) do
305 | @fanout_exchange.publish(hash.to_json, headers: {"hoge": "fuga"})
306 | end
307 |
308 | d.events.each do |event|
309 | assert_equal expect_hash, event[2]
310 | end
311 | end
312 |
313 | def test_include_delivery_info
314 | conf = CONFIG.clone.gsub(/queue\stest_in_fanout/, "queue test_in_topic")
315 | conf << "\ninclude_delivery_info true\n"
316 | d = create_driver(conf)
317 |
318 | queue = @channel.queue("test_in_topic")
319 | topic_exchange = Bunny::Exchange.new(@channel, "topic", "test_in_topic")
320 | queue.bind(topic_exchange, routing_key: "test_in_topic")
321 |
322 | delivery_info = { "exchange" => "test_in_topic", "routing_key" => "test_in_topic" }
323 | expect_hash = {"foo" => "bar"}
324 | expect_hash["delivery_info"] = delivery_info
325 | d.run(expect_emits: 1) do
326 | @topic_exchange.publish(expect_hash.to_json, routing_key: "test_in_topic")
327 | end
328 |
329 | d.events.each do |event|
330 | # assert_equal expect_hash, event[2]
331 | assert_equal expect_hash["foo"], event[2]["foo"]
332 | assert_equal expect_hash["delivery_info"][":exchange"], event[2]["delivery_info"][":exchange"]
333 | assert_equal expect_hash["delivery_info"][":routing_key"], event[2]["delivery_info"][":routing_key"]
334 | end
335 | end
336 |
337 | def test_manual_ack
338 | conf = CONFIG.clone
339 | conf << "\nmanual_ack true"
340 | d = create_driver(conf)
341 | expect_hash = {"foo" => "bar"}
342 | d.run(expect_emits: 1) do
343 | @fanout_exchange.publish(expect_hash.to_json)
344 | end
345 |
346 | d.events.each do |event|
347 | assert_equal expect_hash, event[2]
348 | end
349 | end
350 | end
351 |
--------------------------------------------------------------------------------