rabbitmq_client 0.0.0.pre → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.reek.yml +21 -0
  4. data/.travis.yml +12 -1
  5. data/README.md +129 -17
  6. data/bin/setup +4 -0
  7. data/lib/rabbitmq_client/callback.rb +47 -0
  8. data/lib/rabbitmq_client/exchange.rb +25 -0
  9. data/lib/rabbitmq_client/exchange_registry.rb +33 -0
  10. data/lib/rabbitmq_client/json_formatter.rb +29 -0
  11. data/lib/rabbitmq_client/json_log_subscriber.rb +90 -0
  12. data/lib/rabbitmq_client/lifecycle.rb +53 -0
  13. data/lib/rabbitmq_client/log_subscriber_base.rb +16 -0
  14. data/lib/rabbitmq_client/logger_builder.rb +35 -0
  15. data/lib/rabbitmq_client/message_publisher.rb +57 -0
  16. data/lib/rabbitmq_client/plain_log_subscriber.rb +64 -0
  17. data/lib/rabbitmq_client/plugin.rb +31 -0
  18. data/lib/rabbitmq_client/publisher.rb +79 -0
  19. data/lib/rabbitmq_client/tags_filter.rb +16 -0
  20. data/lib/rabbitmq_client/text_formatter.rb +42 -0
  21. data/lib/rabbitmq_client/version.rb +2 -1
  22. data/lib/rabbitmq_client.rb +99 -2
  23. data/rabbitmq_client.gemspec +1 -0
  24. data/script/travis.sh +2 -0
  25. data/spec/callback_spec.rb +31 -0
  26. data/spec/exchange_registry_spec.rb +32 -0
  27. data/spec/json_formatter_spec.rb +43 -0
  28. data/spec/json_log_subscriber_spec.rb +145 -0
  29. data/spec/lifecycle_spec.rb +78 -0
  30. data/spec/plain_log_subscriber_spec.rb +115 -0
  31. data/spec/plugin_spec.rb +12 -0
  32. data/spec/publisher_spec.rb +150 -0
  33. data/spec/rabbitmq_client_spec.rb +83 -0
  34. data/spec/support/dummy_rabbitmq_client_plugin.rb +13 -0
  35. data/spec/tags_filter_spec.rb +37 -0
  36. data/spec/text_formatter_spec.rb +45 -0
  37. metadata +57 -5
  38. data/Gemfile.lock +0 -156
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'log_subscriber_base'
4
+
5
+ module RabbitmqClient
6
+ # Manage RabbitmqClient plain text logs
7
+ class PlainLogSubscriber < LogSubscriberBase
8
+ def publisher_created(event)
9
+ msg = 'The RabbitmqClient publisher is created ' \
10
+ "with the follwong configs #{event.payload.inspect}"
11
+ debug(msg)
12
+ end
13
+
14
+ def network_error(event)
15
+ payload = event.payload
16
+ msg = "Failed to publish a message (#{payload.fetch(:error).message}) " \
17
+ "to exchange (#{payload.dig(:options, :exchange_name)})"
18
+ error(msg)
19
+ end
20
+
21
+ def overriding_configs(event)
22
+ msg = 'Overriding the follwing configs for ' \
23
+ "the created publisher #{event.payload.inspect}"
24
+ debug(msg)
25
+ end
26
+
27
+ def publishing_message(event)
28
+ payload = event.payload
29
+ msg = 'Start>> Publishing a new message ' \
30
+ "(message_id: #{payload.fetch(:message_id, 'undefined')} ) " \
31
+ "to the exchange (#{payload.fetch(:exchange, 'undefined')})"
32
+ debug(msg)
33
+ end
34
+
35
+ def published_message(event)
36
+ payload = event.payload
37
+ msg = '<<DONE Published a message to ' \
38
+ "the exchange (#{payload.fetch(:exchange, 'undefined')}) " \
39
+ "with message_id: #{payload.fetch(:message_id, 'undefined')}"
40
+ info(msg)
41
+ end
42
+
43
+ def confirming_message(event)
44
+ msg = 'Start>> confirming a message ' \
45
+ "(message_id: #{event.payload.fetch(:message_id, 'undefined')})"
46
+ debug(msg)
47
+ end
48
+
49
+ def message_confirmed(event)
50
+ msg_id = event.payload.fetch(:message_id, 'undefined')
51
+ msg = '<<DONE confirmed a message ' \
52
+ "(message_id: #{msg_id}) Successfuly"
53
+ debug(msg)
54
+ end
55
+
56
+ def exhange_not_found(event)
57
+ error("The Exchange '#{event.payload.fetch(:name)}' cannot be found")
58
+ end
59
+
60
+ def created_exhange(event)
61
+ debug("The #{event.payload.fetch(:name)} exchange is created successfuly")
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/class/attribute'
4
+
5
+ module RabbitmqClient
6
+ # Custom Error thrown in case of defining a plugin without any callbacks
7
+ class EmptyPlugin < RuntimeError
8
+ def initialize(name)
9
+ super("The Plugin '#{name}' is empty")
10
+ end
11
+ end
12
+ # Plugin class is the base class for all Plugins that
13
+ # extends RabbitmqClient functionalty.
14
+ class Plugin
15
+ def initialize
16
+ callback_block.call(RabbitmqClient.lifecycle)
17
+ end
18
+
19
+ def callback_block
20
+ klass = self.class
21
+ klass.callback_block || (raise EmptyPlugin, klass.to_s)
22
+ end
23
+
24
+ class << self
25
+ attr_accessor :callback_block
26
+ def callbacks(&block)
27
+ @callback_block = block
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message_publisher'
4
+
5
+ module RabbitmqClient
6
+ # Publisher class is responsible for publishing events to rabbitmq exhanges
7
+ class Publisher
8
+ def initialize(**config)
9
+ @config = config
10
+ @session_params = session_params
11
+ @exchange_registry = @config.fetch(:exchange_registry, nil)
12
+ @session_params.freeze
13
+ @session_pool = create_connection_pool
14
+ notify('publisher_created', @session_params)
15
+ end
16
+
17
+ def publish(data, options)
18
+ return nil unless @exchange_registry
19
+
20
+ handle_publish_event(data, options)
21
+ rescue StandardError => e
22
+ notify('network_error', error: e, options: options)
23
+ raise
24
+ end
25
+
26
+ private
27
+
28
+ def overwritten_config_notification
29
+ return unless overwritten_config?
30
+
31
+ notify('overriding_configs',
32
+ threaded: false,
33
+ automatically_recover: false)
34
+ end
35
+
36
+ def overwritten_config?
37
+ @config.dig(:session_params, :threaded) ||
38
+ @config.dig(:session_params, :automatically_recover)
39
+ end
40
+
41
+ def session_params
42
+ overwritten_config_notification
43
+ @config.fetch(:session_params, {})
44
+ .merge(threaded: false,
45
+ automatically_recover: false,
46
+ heartbeat: @config.dig(
47
+ :session_params, :heartbeat_publisher
48
+ ) || 0)
49
+ end
50
+
51
+ def handle_publish_event(data, options)
52
+ exchange = @exchange_registry.find(options.fetch(:exchange_name, nil))
53
+ @session_pool.with do |session|
54
+ session.start
55
+ channel = session.create_channel
56
+ channel.confirm_select
57
+ message = MessagePublisher.new(data, exchange, channel, options)
58
+ message.publish
59
+ message.wait_for_confirms
60
+ channel.close
61
+ end
62
+ end
63
+
64
+ def create_connection_pool
65
+ pool_size = @session_params.fetch(:session_pool, 1)
66
+ ConnectionPool.new(size: pool_size) do
67
+ Bunny.new(@config[:rabbitmq_url],
68
+ { logger: RabbitmqClient.logger }.merge(@session_params))
69
+ end
70
+ end
71
+
72
+ def notify(event, payload = {})
73
+ ActiveSupport::Notifications.instrument(
74
+ "#{event}.rabbitmq_client",
75
+ payload
76
+ )
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RabbitmqClient
4
+ # ExchangeRegistry is a store for all managed exchanges and their details
5
+ class TagsFilter
6
+ def self.tags
7
+ config = RabbitmqClient.config
8
+ global_store = config.global_store
9
+ return unless global_store
10
+
11
+ global_store.store.select do |key, _value|
12
+ Array(config.whitelist).include? key.downcase.to_sym
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'English'
5
+
6
+ module RabbitmqClient
7
+ # Formatter for text log messages
8
+ class TextFormatter < ::Logger::Formatter
9
+ def initialize
10
+ @datetime_format = nil
11
+ @severity_text = nil
12
+ @tags = nil
13
+ super
14
+ end
15
+
16
+ def call(severity, time, progname, msg)
17
+ create_instance_vars(severity)
18
+ format(Format,
19
+ @severity_text[0],
20
+ format_datetime(time),
21
+ $PID,
22
+ @severity_text,
23
+ progname,
24
+ "#{@tags}#{msg2str(msg)}")
25
+ end
26
+
27
+ private
28
+
29
+ def create_instance_vars(severity)
30
+ @severity_text = if severity.is_a?(Integer)
31
+ Logger::Severity.constants(false).select do |level|
32
+ Logger::Severity.const_get(level) == severity
33
+ end.first.to_s
34
+ else
35
+ severity
36
+ end
37
+ @tags = (TagsFilter.tags || {}).collect do |key, val|
38
+ "[#{key}: #{val}] "
39
+ end.join
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nodoc:
3
4
  module RabbitmqClient
4
- VERSION = '0.0.0.pre'
5
+ VERSION = '0.0.1'
5
6
  end
@@ -1,8 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support'
4
+ require 'bunny'
5
+ require 'connection_pool'
6
+
3
7
  require 'rabbitmq_client/version'
8
+ require 'rabbitmq_client/lifecycle'
9
+ require 'rabbitmq_client/plugin'
10
+ require 'rabbitmq_client/exchange_registry'
11
+ require 'rabbitmq_client/publisher'
4
12
 
13
+ # RabbitmqClient Module is used as a clinet library for Rabbitmq
14
+ # This Module is supporting the following use cases
15
+ # - Publish events to Rabbitmq server
5
16
  module RabbitmqClient
6
- class Error < StandardError; end
7
- # Your code goes here...
17
+ extend ActiveSupport::Autoload
18
+
19
+ include ActiveSupport::Configurable
20
+ include ActiveSupport::JSON
21
+
22
+ eager_autoload do
23
+ autoload :LoggerBuilder
24
+ autoload :PlainLogSubscriber
25
+ autoload :JsonLogSubscriber
26
+ autoload :JsonFormatter
27
+ autoload :TextFormatter
28
+ autoload :TagsFilter
29
+ end
30
+
31
+ @exchange_registry = ExchangeRegistry.new
32
+ # [url] url address of rabbitmq server
33
+ config_accessor(:rabbitmq_url, instance_accessor: false) do
34
+ 'amqp://guest:guest@127.0.0.1:5672'
35
+ end
36
+
37
+ # [logger_configs] configs for teh used logger
38
+ # logs_format: json, plain
39
+ # logs_to_stdout: true, false
40
+ # logs_level: info, debug
41
+ # logs_filename: logs file name
42
+ config_accessor(:logger_configs, instance_accessor: false) do
43
+ {
44
+ logs_format: 'plain',
45
+ logs_level: :info,
46
+ logs_filename: nil,
47
+ logger: nil
48
+ }
49
+ end
50
+ # default rabbitmq configs
51
+ # heartbeat_publisher = 0
52
+ # session_pool = 1
53
+ config_accessor(:session_params, instance_accessor: false) do
54
+ {
55
+ heartbeat_publisher: 0,
56
+ session_pool: 1
57
+ }
58
+ end
59
+
60
+ config_accessor(:plugins, instance_accessor: false) { [] }
61
+ config_accessor(:global_store, instance_accessor: false) { nil }
62
+ config_accessor(:whitelist, instance_accessor: false) do
63
+ ['x-request-id'.to_sym]
64
+ end
65
+
66
+ class << self
67
+ def add_exchange(name, type, options = {})
68
+ @exchange_registry.add(name, type, options)
69
+ end
70
+
71
+ def publish(payload, options = {})
72
+ publisher.publish(payload, options)
73
+ end
74
+
75
+ def lifecycle
76
+ @lifecycle ||= setup_lifecycle
77
+ end
78
+
79
+ def logger
80
+ @logger ||= setup_logger
81
+ end
82
+
83
+ private
84
+
85
+ def setup_logger
86
+ LoggerBuilder.new(config[:logger_configs]).build_logger
87
+ end
88
+
89
+ def publisher
90
+ @publisher ||= init_publisher
91
+ end
92
+
93
+ def init_publisher
94
+ Publisher.new(config.merge(
95
+ exchange_registry: @exchange_registry
96
+ ))
97
+ end
98
+
99
+ def setup_lifecycle
100
+ @lifecycle = Lifecycle.new
101
+ plugins.each(&:new)
102
+ @lifecycle
103
+ end
104
+ end
8
105
  end
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'rubycritic-small-badge', '~> 0.2.1'
33
33
  spec.add_development_dependency 'simplecov', '~> 0.17.1'
34
34
  spec.add_development_dependency 'simplecov-small-badge', '~> 0.2.4'
35
+ spec.add_development_dependency 'solargraph', '~> 0.37.2'
35
36
  spec.add_development_dependency 'timecop', '~> 0.9.1'
36
37
 
37
38
  spec.files = `git ls-files`.split("\n")
data/script/travis.sh CHANGED
@@ -13,6 +13,8 @@ if [ -z "$QUICK" ]; then
13
13
  echo "Pushing badges upstream"
14
14
  [ -d badges ] || mkdir badges
15
15
  cp coverage/coverage_badge* badges/ 2>/dev/null || true
16
+ cp -r coverage coverage_info 2>/dev/null || true
17
+ cp -r tmp/rubycritic rubycritic 2>/dev/null || true
16
18
  fi
17
19
 
18
20
  fi
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative 'support/dummy_rabbitmq_client_plugin'
5
+
6
+ describe RabbitmqClient::Callback do
7
+ subject { described_class.new }
8
+
9
+ let(:callback) { ->(*_args) {} }
10
+
11
+ it 'initialize with emprty events' do
12
+ expect(subject.instance_variable_get(:@before).count).to eq 0
13
+ expect(subject.instance_variable_get(:@after).count).to eq 0
14
+ end
15
+
16
+ it 'raises for unsupported events' do
17
+ expect do
18
+ subject.add(:execute, &callback)
19
+ end.to raise_error(
20
+ RabbitmqClient::InvalidCallback, /Invalid callback type: execute/
21
+ )
22
+ end
23
+
24
+ it 'add before and after events' do
25
+ expect { subject.add(:before, &callback) }.not_to raise_error
26
+ expect { subject.add(:after, &callback) }.not_to raise_error
27
+
28
+ expect(subject.instance_variable_get(:@before).count).to eq 1
29
+ expect(subject.instance_variable_get(:@after).count).to eq 1
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RabbitmqClient::ExchangeRegistry do
6
+ let(:registry) { described_class.new }
7
+
8
+ let(:exchange_name) { 'tmp_exchange' }
9
+ let(:exchange_type) { 'test' }
10
+ let(:exchange_opt) { { opt: false } }
11
+
12
+ it 'initialize with emprty registry' do
13
+ expect(registry.instance_variable_get(:@exchanges)).to be_empty
14
+ end
15
+
16
+ it 'add and find exchanges' do
17
+ expect do
18
+ registry.add(exchange_name, exchange_type, exchange_opt)
19
+ end.not_to raise_error
20
+
21
+ exchange = registry.find(exchange_name)
22
+ expect(exchange.name).to eq exchange_name
23
+ expect(exchange.type).to eq exchange_type
24
+ expect(exchange.options).to eq exchange_opt
25
+ end
26
+
27
+ it 'raise error for unknown exchanges' do
28
+ expect do
29
+ registry.find(exchange_name)
30
+ end.to raise_error(described_class::ExchangeNotFound)
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RabbitmqClient::JsonFormatter do
6
+ subject { described_class.new }
7
+ let(:message) { 'Test loG message' }
8
+ let(:global_store) { double('Store') }
9
+ let(:time) { Time.now }
10
+ let(:formated_time) { time.strftime('%Y-%m-%dT%H:%M:%S.%6N ') }
11
+
12
+ before do
13
+ RabbitmqClient.config.global_store = global_store
14
+ end
15
+
16
+ after do
17
+ RabbitmqClient.config.global_store = nil
18
+ end
19
+ describe '.call' do
20
+ it 'formatt the log message' do
21
+ allow(global_store).to receive(:store).and_return({})
22
+ log_line = subject.call('DEBUG', time, nil, message)
23
+ json_log = JSON.parse(log_line[0...-1])
24
+ expect(json_log).to eq(
25
+ 'level' => 'DEBUG',
26
+ 'message' => 'Test loG message',
27
+ 'timestamp' => formated_time
28
+ )
29
+ end
30
+
31
+ it 'add tags to the log message' do
32
+ allow(global_store).to receive(:store).and_return('x-request-id': '10')
33
+ log_line = subject.call('DEBUG', time, nil, message)
34
+ json_log = JSON.parse(log_line[0...-1])
35
+ expect(json_log).to eq(
36
+ 'level' => 'DEBUG',
37
+ 'message' => 'Test loG message',
38
+ 'timestamp' => formated_time,
39
+ 'x-request-id' => '10'
40
+ )
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RabbitmqClient::JsonLogSubscriber do
6
+ class DummyEvent < ActiveSupport::Notifications::Event
7
+ def initialize(event_type, payload = {})
8
+ super(event_type, Time.now, Time.now, 1, payload)
9
+ end
10
+ end
11
+
12
+ let(:error) { double('error') }
13
+
14
+ before do
15
+ allow(error).to receive(:message).and_return('error')
16
+ end
17
+
18
+ describe '#publisher_created' do
19
+ it 'send logger a debug message' do
20
+ expect(subject.logger).to receive(:debug).with(
21
+ action: 'publisher_created',
22
+ message: 'The RabbitmqClient publisher is created',
23
+ publisher_configs: {},
24
+ source: 'rabbitmq_client'
25
+ )
26
+ subject.publisher_created(DummyEvent.new(
27
+ 'publisher_created.rabbitmq_client',
28
+ {}
29
+ ))
30
+ end
31
+ end
32
+
33
+ describe '#network_error' do
34
+ let(:payload) { { error: error, options: { exchange_name: 'exchange' } } }
35
+ it 'send logger a error message' do
36
+ expect(subject.logger).to receive(:error).with(
37
+ action: 'network_error',
38
+ error_message: 'error',
39
+ exchange_name: 'undefined',
40
+ message: 'Failed to publish a message',
41
+ message_id: 'undefined', source: 'rabbitmq_client'
42
+ )
43
+ subject.network_error(
44
+ DummyEvent.new('network_error.rabbitmq_client', payload)
45
+ )
46
+ end
47
+ end
48
+ describe '#overriding_configs' do
49
+ it 'send logger a debug message' do
50
+ expect(subject.logger).to receive(:debug).with(
51
+ action: 'overriding_configs',
52
+ message: 'Overriding publisher configs',
53
+ publisher_configs: {},
54
+ source: 'rabbitmq_client'
55
+ )
56
+ subject.overriding_configs(
57
+ DummyEvent.new('overriding_configs.rabbitmq_client', {})
58
+ )
59
+ end
60
+ end
61
+ describe '#publishing_message' do
62
+ it 'send logger a debug message' do
63
+ expect(subject.logger).to receive(:debug).with(
64
+ action: 'publishing_message',
65
+ exchange_name: 'undefined',
66
+ message: 'Publishing a new message',
67
+ message_id: 'undefined',
68
+ source: 'rabbitmq_client'
69
+ )
70
+ subject.publishing_message(
71
+ DummyEvent.new('publishing_message.rabbitmq_client', {})
72
+ )
73
+ end
74
+ end
75
+ describe '#published_message' do
76
+ it 'send logger a debug message' do
77
+ expect(subject.logger).to receive(:info).with(
78
+ action: 'published_message',
79
+ exchange_name: 'undefined',
80
+ message: 'Published a message',
81
+ message_id: 'undefined',
82
+ source: 'rabbitmq_client'
83
+ )
84
+ subject.published_message(
85
+ DummyEvent.new('published_message.rabbitmq_client', {})
86
+ )
87
+ end
88
+ end
89
+ describe '#confirming_message' do
90
+ it 'send logger a debug message' do
91
+ expect(subject.logger).to receive(:debug).with(
92
+ action: 'confirming_message',
93
+ exchange_name: 'undefined',
94
+ message: 'Confirming a message',
95
+ message_id: 'undefined',
96
+ source: 'rabbitmq_client'
97
+ )
98
+ subject.confirming_message(
99
+ DummyEvent.new('confirming_message.rabbitmq_client', {})
100
+ )
101
+ end
102
+ end
103
+ describe '#message_confirmed' do
104
+ it 'send logger a debug message' do
105
+ expect(subject.logger).to receive(:debug).with(
106
+ action: 'message_confirmed',
107
+ exchange_name: 'undefined',
108
+ message: 'Confirmed a message',
109
+ message_id: 'undefined',
110
+ source: 'rabbitmq_client'
111
+ )
112
+ subject.message_confirmed(
113
+ DummyEvent.new('message_confirmed.rabbitmq_client', {})
114
+ )
115
+ end
116
+ end
117
+ describe '#exhange_not_found' do
118
+ let(:payload) { { name: 'exchange' } }
119
+ it 'send logger a error message' do
120
+ expect(subject.logger).to receive(:error).with(
121
+ action: 'exhange_not_found',
122
+ exchange_name: 'exchange',
123
+ message: 'Exhange Not Found',
124
+ source: 'rabbitmq_client'
125
+ )
126
+ subject.exhange_not_found(
127
+ DummyEvent.new('exhange_not_found.rabbitmq_client', payload)
128
+ )
129
+ end
130
+ end
131
+ describe '#created_exhange' do
132
+ let(:payload) { { name: 'exchange' } }
133
+ it 'send logger a debug message' do
134
+ expect(subject.logger).to receive(:debug).with(
135
+ action: 'created_exhange',
136
+ exchange_name: 'exchange',
137
+ message: 'Exhange is created successfuly',
138
+ source: 'rabbitmq_client'
139
+ )
140
+ subject.created_exhange(
141
+ DummyEvent.new('created_exhange.rabbitmq_client', payload)
142
+ )
143
+ end
144
+ end
145
+ end