fare 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ require "logger"
2
+
3
+ module Fare
4
+ module Middleware
5
+
6
+ # Use this to log all events and stacktraces:
7
+ #
8
+ # Usage:
9
+ #
10
+ # use Fare::Middleware::Logging, logger: Logger.new($stdout)
11
+ class Logging
12
+
13
+ def initialize(app, options = {})
14
+ @app = app
15
+ @logger = options.fetch(:logger)
16
+ end
17
+
18
+ def call(env)
19
+ attrs = env.fetch(:event).attributes
20
+ start = Time.now
21
+ begin
22
+ @app.call(env)
23
+ rescue Exception => exception
24
+ duration = Time.now - start
25
+ @logger.warn("Handled event: %s" % {
26
+ event: attrs,
27
+ result: "failure",
28
+ duration: duration,
29
+ error_class: exception.class.name,
30
+ error_message: exception.message,
31
+ backtrace: exception.backtrace.to_a,
32
+ }.to_json)
33
+ raise
34
+ else
35
+ duration = Time.now - start
36
+ @logger.info("Handled event: %s" % {
37
+ event: attrs,
38
+ result: "success",
39
+ duration: duration,
40
+ }.to_json)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ require 'newrelic_rpm'
2
+ ::NewRelic::Agent.manual_start
3
+
4
+ module Fare
5
+ module Middleware
6
+
7
+ # Add NewRelic monitoring to the subscriber.
8
+ # Make sure you have a newrelic.yml in your config dir.
9
+ #
10
+ # Usage:
11
+ #
12
+ # subscriber do
13
+ # setup do
14
+ # require "fare/middleware/newrelic"
15
+ # end
16
+ # always_run do
17
+ # use Fare::Middleware::NewRelic
18
+ # end
19
+ # end
20
+ class NewRelic
21
+ include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation
22
+
23
+ def initialize(app, options = {})
24
+ @app = app
25
+ at_exit { ::NewRelic::Agent.shutdown }
26
+ end
27
+
28
+ def call(env)
29
+ @app.call(env)
30
+ end
31
+ add_transaction_tracer :call, :category => :task
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Fare
2
+ module Middleware
3
+
4
+ # Use this to send errors to Sentry:
5
+ #
6
+ # Usage:
7
+ #
8
+ # use Fare::Middleware::Raven, dsn: "http://...", logger: logger, environment: "production"
9
+ class Raven
10
+
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ require "raven" unless defined?(::Raven)
14
+ configure_sentry(options)
15
+ end
16
+
17
+ def call(env)
18
+ ::Raven.extra_context(serialize(env))
19
+ @app.call(env)
20
+ rescue Exception => exception
21
+ ::Raven.capture_exception(exception)
22
+ raise
23
+ ensure
24
+ ::Raven::Context.clear!
25
+ end
26
+
27
+ private
28
+
29
+ def configure_sentry(options)
30
+ ::Raven.configure do |config|
31
+ config.dsn = options.fetch(:dsn)
32
+ config.logger = options.fetch(:logger)
33
+ config.environments = [options.fetch(:environment)]
34
+ config.excluded_exceptions = []
35
+ end
36
+ end
37
+
38
+ # removes objects and makes normal hashes, so sentry can read the better
39
+ def serialize(env)
40
+ JSON.parse(env.to_json)
41
+ rescue
42
+ env
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,65 @@
1
+ module Fare
2
+ UnknownTopicError = Class.new(RuntimeError)
3
+
4
+ class Publisher
5
+
6
+ attr_reader :configuration, :options
7
+
8
+ def initialize(configuration, options)
9
+ @configuration = configuration
10
+ @options = options
11
+ end
12
+
13
+ def call
14
+ ensure_topic_publishable!
15
+ topic.publish(event.serialize)
16
+ end
17
+
18
+ def topic
19
+ Fare.topic_adapter.fetch(topic_info.fetch("arn"))
20
+ end
21
+
22
+ def topic_info
23
+ @topic_info ||= configuration.find_publishable_topic(options)
24
+ end
25
+
26
+ def event
27
+ Event.new(
28
+ id: "event-#{SecureRandom.uuid}",
29
+ subject: subject,
30
+ action: action,
31
+ payload: payload,
32
+ source: app_name,
33
+ version: version,
34
+ sent_at: Time.now.utc,
35
+ )
36
+ end
37
+
38
+ def subject
39
+ options.fetch(:subject)
40
+ end
41
+
42
+ def action
43
+ options.fetch(:action)
44
+ end
45
+
46
+ def payload
47
+ options.fetch(:payload)
48
+ end
49
+
50
+ def version
51
+ topic_info.fetch("version")
52
+ end
53
+
54
+ def app_name
55
+ configuration.app_name
56
+ end
57
+
58
+ def ensure_topic_publishable!
59
+ unless topic_info
60
+ raise UnknownTopicError, "Topic with subject #{subject} and action #{action} not in lock file, maybe run `fare update`"
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ module Fare
2
+ class QueueAdapter
3
+
4
+ def self.fetch(*args)
5
+ new(*args).queue
6
+ end
7
+
8
+ attr_reader :environment, :name
9
+
10
+ def initialize(environment, name)
11
+ @environment = environment
12
+ @name = name
13
+ end
14
+
15
+ def queue
16
+ sqs.queues.named(queue_name)
17
+ rescue AWS::SQS::Errors::NonExistentQueue
18
+ sqs.queues.create(queue_name)
19
+ end
20
+
21
+ def queue_name
22
+ "#{environment}-#{name}"
23
+ end
24
+
25
+ def sqs
26
+ AWS::SQS.new
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,85 @@
1
+ begin
2
+ require "json_expressions"
3
+ rescue LoadError
4
+ raise "Please install the gem `json_expressions` to use fare/rspec"
5
+ end
6
+
7
+ module Fare
8
+ module FareMatcher
9
+
10
+ def publish(subject, action, payload = nil)
11
+ Expectation.new(subject, action, payload)
12
+ end
13
+
14
+ class Expectation
15
+
16
+ attr_reader :subject, :action, :payload
17
+
18
+ def initialize(subject, action, payload)
19
+ @subject = subject
20
+ @action = action
21
+ @payload = payload
22
+ end
23
+
24
+ def supports_block_expectations?
25
+ true
26
+ end
27
+
28
+ def matches?(expect_block)
29
+ Fare.stubbed_messages.clear
30
+ expect_block.call
31
+ !event.nil? && payload_matches?
32
+ end
33
+
34
+ def does_not_match?(expect_block)
35
+ raise ArgumentError, "Checking payload with negative expectation is not possible" if payload
36
+ Fare.stubbed_messages.clear
37
+ expect_block.call
38
+ event.nil?
39
+ end
40
+
41
+ def failure_message
42
+ if event.nil?
43
+ if Fare.stubbed_messages.size == 0
44
+ "There were no events published"
45
+ else
46
+ "Expected event #{event_name}, but got:\n#{Fare.stubbed_messages.list}"
47
+ end
48
+ else
49
+ "Payload did not match: #{payload_matcher.last_error}\nActual payload was:\n#{event.payload}"
50
+ end
51
+ end
52
+
53
+ def failure_message_when_negated
54
+ "Expected not to publish #{event_name}, but published it with payload: #{event.payload}"
55
+ end
56
+
57
+ def event_name
58
+ "#{subject}##{action}"
59
+ end
60
+
61
+ def payload_matches?
62
+ if payload
63
+ payload_matcher =~ event.payload
64
+ else
65
+ true
66
+ end
67
+ end
68
+
69
+ def event
70
+ Fare.stubbed_messages.get(subject, action)
71
+ end
72
+
73
+ def payload_matcher
74
+ @payload_matcher ||= JsonExpressions::Matcher.new(payload)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ RSpec.configure do |config|
84
+ config.include Fare::FareMatcher
85
+ end
@@ -0,0 +1,35 @@
1
+ require "timeout"
2
+
3
+ module Fare
4
+ class Subscriber
5
+ UnknownSubscriber = Class.new(ArgumentError)
6
+
7
+ attr_reader :configuration
8
+
9
+ def initialize(configuration, options = {})
10
+ name = (options[:name] || configuration.app_name).to_s
11
+ subscriber_config = configuration.fetch_subscriber(name)
12
+ subscriber_config.load_setup
13
+ @sqs_queue = configuration.fetch_subscriber_queue(name)
14
+ @stacks = subscriber_config.stacks
15
+ end
16
+
17
+ def produce(queue)
18
+ message = @sqs_queue.receive_message(attributes: [:all])
19
+ if message
20
+ queue << message
21
+ end
22
+ end
23
+
24
+ def consume(message)
25
+ event = Event.deserialize(message.body)
26
+ @stacks.each do |stack|
27
+ if stack.handles?(event)
28
+ stack.to_app.call(event: event)
29
+ message.delete
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,270 @@
1
+ require "daemonic"
2
+
3
+ module Fare
4
+ class SubscriberCLI
5
+
6
+ attr_reader :argv, :command
7
+
8
+ def initialize(argv)
9
+ @command = argv.shift
10
+ @argv = argv
11
+ end
12
+
13
+ def call
14
+ case command
15
+ when nil, "-h", "--help", "help"
16
+ show_help
17
+ exit 0
18
+ when "start"
19
+ start
20
+ when "stop"
21
+ stop
22
+ when "status"
23
+ status
24
+ when "restart"
25
+ restart
26
+ else
27
+ puts "Unkown command: #{command}"
28
+ puts
29
+ show_help
30
+ exit 1
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def start
37
+ fare_options = {
38
+ filename: Fare.default_config_file,
39
+ environment: (Fare.default_environment || "development"),
40
+ }
41
+ daemon_options = {
42
+ concurrency: 4,
43
+ startup_timeout: 20,
44
+ }
45
+ subscriber_options = {}
46
+ optparser = OptionParser.new do |parser|
47
+
48
+ parser.banner = "Usage: fare subscriber start [options]"
49
+
50
+
51
+ parser.on "-E", "--environment ENV", "Set the environment (default: #{fare_options[:environment]})" do |environment|
52
+ fare_options[:environment] = environment
53
+ end
54
+
55
+ parser.on "--filename FILENAME", "Location of the Fare configuration file (default: #{fare_options[:filename]})" do |filename|
56
+ fare_options[:filename] = filename
57
+ end
58
+
59
+ parser.on "--name NAME", "Which named subscriber to run (default is the unnamed one)" do |name|
60
+ subscriber_options[:name] = name
61
+ end
62
+
63
+ parser.separator ""
64
+ parser.separator "Process options:"
65
+
66
+ parser.on "-c", "--concurrency NUM", Integer, "Set the number of threads (default: #{daemon_options[:concurrency]}" do |concurrency|
67
+ daemon_options[:concurrency] = concurrency
68
+ end
69
+
70
+ parser.on "-P", "--pid FILE", "The location of the PID file (required when daemonizing)" do |pid|
71
+ daemon_options[:pid] = pid
72
+ end
73
+
74
+ parser.on "--[no-]daemonize", "Should the subscriber detach?" do |daemonize|
75
+ daemon_options[:daemonize] = daemonize
76
+ end
77
+
78
+ parser.on "--startup-timeout TIMEOUT", Integer, "How long to wait for the process to start (default: #{daemon_options[:startup_timeout]})" do |timeout|
79
+ daemon_options[:startup_timeout] = timeout
80
+ end
81
+
82
+ parser.separator ""
83
+ parser.separator "Logging options:"
84
+
85
+ parser.on "--log FILE", "Where to log to" do |log|
86
+ daemon_options[:log] = log
87
+ end
88
+
89
+ parser.on "--log-level LEVEL", %w(debug info warn fatal), "Set the log level (default: info)" do |level|
90
+ daemon_options[:log_level] = Logger.const_get(level.upcase)
91
+ end
92
+
93
+ parser.separator ""
94
+ parser.separator "Other options:"
95
+
96
+ parser.on_tail "-h", "--help", "Shows this help page" do
97
+ puts parser
98
+ exit
99
+ end
100
+
101
+ end
102
+
103
+ optparser.parse!(argv)
104
+
105
+
106
+ Daemonic.start(daemon_options) {
107
+ Fare.config(fare_options)
108
+ Subscriber.new(Fare.configuration, subscriber_options)
109
+ }
110
+ end
111
+
112
+ def stop
113
+ daemon_options = {
114
+ stop_timeout: 10
115
+ }
116
+ optparser = OptionParser.new do |parser|
117
+
118
+ parser.banner = "Usage: fare subscriber stop [options]"
119
+
120
+
121
+ parser.separator ""
122
+ parser.separator "Process options:"
123
+
124
+ parser.on "-P", "--pid FILE", "The location of the PID file" do |pid|
125
+ daemon_options[:pid] = pid
126
+ end
127
+
128
+ parser.on "--stop-timeout TIMEOUT", Integer, "How long to wait for the process to stop (default: #{daemon_options[:stop_timeout]})" do |timeout|
129
+ daemon_options[:stop_timeout] = timeout
130
+ end
131
+
132
+ parser.separator ""
133
+ parser.separator "Other options:"
134
+
135
+ parser.on_tail "-h", "--help", "Shows this help page" do
136
+ puts parser
137
+ exit
138
+ end
139
+
140
+ end
141
+
142
+ optparser.parse!(argv)
143
+
144
+ Daemonic.stop(daemon_options)
145
+ end
146
+
147
+ def restart
148
+ fare_options = {
149
+ filename: Fare.default_config_file,
150
+ environment: (Fare.default_environment || "development"),
151
+ }
152
+ daemon_options = {
153
+ concurrency: 4,
154
+ stop_timeout: 10,
155
+ startup_timeout: 20,
156
+ }
157
+ subscriber_options = {}
158
+ optparser = OptionParser.new do |parser|
159
+
160
+ parser.banner = "Usage: fare subscriber start [options]"
161
+
162
+ parser.on "-E", "--environment ENV", "Set the environment (default: #{fare_options[:environment]})" do |environment|
163
+ fare_options[:environment] = environment
164
+ end
165
+
166
+ parser.on "--filename FILENAME", "Location of the Fare configuration file (default: #{fare_options[:filename]})" do |filename|
167
+ fare_options[:filename] = filename
168
+ end
169
+
170
+ parser.on "--name NAME", "Which named subscriber to run (default is the unnamed one)" do |name|
171
+ subscriber_options[:name] = name
172
+ end
173
+
174
+ parser.separator ""
175
+ parser.separator "Process options:"
176
+
177
+ parser.on "-c", "--concurrency NUM", Integer, "Set the number of threads (default: #{daemon_options[:concurrency]}" do |concurrency|
178
+ daemon_options[:concurrency] = concurrency
179
+ end
180
+
181
+ parser.on "-P", "--pid FILE", "The location of the PID file (required)" do |pid|
182
+ daemon_options[:pid] = pid
183
+ end
184
+
185
+ parser.on "--startup-timeout TIMEOUT", Integer, "How long to wait for the process to start (default: #{daemon_options[:startup_timeout]})" do |timeout|
186
+ daemon_options[:startup_timeout] = timeout
187
+ end
188
+
189
+ parser.on "--stop-timeout TIMEOUT", Integer, "How long to wait for the process to stop (default: #{daemon_options[:stop_timeout]})" do |timeout|
190
+ daemon_options[:stop_timeout] = timeout
191
+ end
192
+
193
+ parser.separator ""
194
+ parser.separator "Logging options:"
195
+
196
+ parser.on "--log FILE", "Where to log to" do |log|
197
+ daemon_options[:log] = log
198
+ end
199
+
200
+ parser.on "--log-level LEVEL", %w(debug info warn fatal), "Set the log level (default: info)" do |level|
201
+ daemon_options[:log_level] = Logger.const_get(level.upcase)
202
+ end
203
+
204
+ parser.separator ""
205
+ parser.separator "Other options:"
206
+
207
+ parser.on_tail "-h", "--help", "Shows this help page" do
208
+ puts parser
209
+ exit
210
+ end
211
+
212
+ end
213
+
214
+ optparser.parse!(argv)
215
+
216
+ Daemonic.restart(daemon_options) {
217
+ Fare.config(fare_options)
218
+ Subscriber.new(Fare.configuration, subscriber_options)
219
+ }
220
+ end
221
+
222
+ def status
223
+ daemon_options = {
224
+ }
225
+ optparser = OptionParser.new do |parser|
226
+
227
+ parser.banner = "Usage: fare subscriber status [options]"
228
+
229
+ parser.separator ""
230
+ parser.separator "Process options:"
231
+
232
+ parser.on "-P", "--pid FILE", "The location of the PID file" do |pid|
233
+ daemon_options[:pid] = pid
234
+ end
235
+
236
+ parser.separator ""
237
+ parser.separator "Other options:"
238
+
239
+ parser.on_tail "-h", "--help", "Shows this help page" do
240
+ puts parser
241
+ exit
242
+ end
243
+
244
+ end
245
+
246
+ optparser.parse!(argv)
247
+
248
+ Daemonic.status(daemon_options)
249
+ end
250
+
251
+ def show_help
252
+ puts <<-HELP.gsub(/^ +/, '')
253
+ Usage: fare subscriber COMMAND [OPTIONS]
254
+
255
+ Available commands:
256
+
257
+ fare subscriber start [options]
258
+ fare subscriber stop [options]
259
+ fare subscriber status [options]
260
+ fare subscriber restart [options]
261
+
262
+ You can run any command with the --help option.
263
+
264
+ Example: fare subscriber start --help
265
+ HELP
266
+ end
267
+
268
+
269
+ end
270
+ end