rabbitmq_client 0.0.0.pre → 0.0.1

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