├── 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 | --------------------------------------------------------------------------------