fare 0.1.0

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