harbinger 0.0.1.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.hound.yml +818 -0
- data/.travis.yml +20 -0
- data/Gemfile +23 -3
- data/README.md +8 -27
- data/Rakefile +47 -1
- data/app/controllers/harbinger/messages_controller.rb +24 -0
- data/app/models/harbinger/database_channel_message.rb +51 -0
- data/app/models/harbinger/database_channel_message_element.rb +19 -0
- data/app/views/harbinger/messages/index.html.erb +43 -0
- data/app/views/harbinger/messages/show.html.erb +24 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20140310185338_create_harbinger_database_channel_message.rb +11 -0
- data/db/migrate/20140310185339_create_harbinger_database_channel_message_elements.rb +14 -0
- data/gemfiles/rails4.1.gemfile +12 -0
- data/gemfiles/rails4.gemfile +13 -0
- data/harbinger.gemspec +22 -7
- data/lib/generators/harbinger/install/install_generator.rb +23 -0
- data/lib/generators/harbinger/install/templates/harbinger_initializer.rb.erb +6 -0
- data/lib/harbinger.rb +100 -1
- data/lib/harbinger/channels.rb +25 -0
- data/lib/harbinger/channels/database_channel.rb +15 -0
- data/lib/harbinger/channels/logger_channel.rb +31 -0
- data/lib/harbinger/channels/null_channel.rb +7 -0
- data/lib/harbinger/configuration.rb +58 -0
- data/lib/harbinger/engine.rb +8 -1
- data/lib/harbinger/exceptions.rb +4 -0
- data/lib/harbinger/message.rb +20 -0
- data/lib/harbinger/reporters.rb +34 -0
- data/lib/harbinger/reporters/exception_reporter.rb +26 -0
- data/lib/harbinger/reporters/null_reporter.rb +14 -0
- data/lib/harbinger/reporters/request_reporter.rb +20 -0
- data/lib/harbinger/reporters/user_reporter.rb +20 -0
- data/lib/harbinger/version.rb +1 -1
- data/run_each_spec_in_isolation +9 -0
- data/script/fast_specs +20 -0
- data/spec/controllers/harbinger/messages_controller_spec.rb +26 -0
- data/spec/features/end_to_end_exception_handling_spec.rb +39 -0
- data/spec/lib/harbinger/channels/database_channel_spec.rb +18 -0
- data/spec/lib/harbinger/channels/logger_channel_spec.rb +21 -0
- data/spec/lib/harbinger/channels/null_channel_spec.rb +8 -0
- data/spec/lib/harbinger/channels_spec.rb +40 -0
- data/spec/lib/harbinger/configuration_spec.rb +53 -0
- data/spec/lib/harbinger/message_spec.rb +15 -0
- data/spec/lib/harbinger/reporters/exception_reporter_spec.rb +24 -0
- data/spec/lib/harbinger/reporters/null_reporter_spec.rb +21 -0
- data/spec/lib/harbinger/reporters/request_reporter_spec.rb +23 -0
- data/spec/lib/harbinger/reporters/user_reporter_spec.rb +17 -0
- data/spec/lib/harbinger/reporters_spec.rb +46 -0
- data/spec/lib/harbinger_spec.rb +60 -0
- data/spec/models/harbinger/database_channel_message_element_spec.rb +16 -0
- data/spec/models/harbinger/database_channel_message_spec.rb +68 -0
- data/spec/routing/harbinger/messages_routing_spec.rb +16 -0
- data/spec/spec_active_record_helper.rb +41 -0
- data/spec/spec_fast_helper.rb +70 -0
- data/spec/spec_slow_helper.rb +57 -0
- data/spec/spec_view_helper.rb +38 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
- data/spec/views/harbinger/messages/index.html.erb_spec.rb +31 -0
- data/spec/views/harbinger/messages/show.html.erb_spec.rb +36 -0
- metadata +205 -20
- data/MIT-LICENSE +0 -20
data/lib/harbinger.rb
CHANGED
@@ -1,5 +1,104 @@
|
|
1
|
-
require "harbinger/engine"
|
1
|
+
require "harbinger/engine" if defined?(Rails)
|
2
2
|
require "harbinger/version"
|
3
|
+
require "harbinger/reporters"
|
4
|
+
require "harbinger/channels"
|
5
|
+
require "harbinger/exceptions"
|
6
|
+
require "harbinger/configuration"
|
3
7
|
|
4
8
|
module Harbinger
|
9
|
+
module_function
|
10
|
+
class << self
|
11
|
+
attr_writer :configuration
|
12
|
+
|
13
|
+
def configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module_function
|
19
|
+
def configure
|
20
|
+
yield(configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Responsible for building a :message from the various :contexts and then
|
24
|
+
# delivering the :message to the appropriate :channels.
|
25
|
+
#
|
26
|
+
# @see .build_message
|
27
|
+
# @see .deliver_message
|
28
|
+
#
|
29
|
+
# @param [Hash] options
|
30
|
+
# @option options [Message] :message The message you want to amend.
|
31
|
+
# If none is provided, then one is created.
|
32
|
+
# @option options [Object, Array<Object>] :contexts One or more Objects that
|
33
|
+
# Harbinger will visit and extract message elements from.
|
34
|
+
# @option options [Symbol, Array<Symbol>] :channels One or more channels that
|
35
|
+
# Harbinger will deliver the :message to
|
36
|
+
def call(options)
|
37
|
+
message = build_message(options)
|
38
|
+
deliver_message(message, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Responsible for building a :message from the various :contexts.
|
42
|
+
#
|
43
|
+
# @see .call
|
44
|
+
#
|
45
|
+
# @param [Hash] options
|
46
|
+
# @option options [Message] :message The message you want to amend.
|
47
|
+
# If none is provided, then one is created.
|
48
|
+
# @option options [Object, Array<Object>] :contexts One or more Objects that
|
49
|
+
# Harbinger will visit and extract message elements from.
|
50
|
+
def build_message(options = {})
|
51
|
+
contexts = Array(options.fetch(:contexts)).flatten.compact
|
52
|
+
message = options.fetch(:message) { default_message }
|
53
|
+
|
54
|
+
contexts.each { |context| reporter_for(context).accept(message) }
|
55
|
+
message
|
56
|
+
end
|
57
|
+
|
58
|
+
# Responsible for delivering a :message to the appropriate :channels.
|
59
|
+
#
|
60
|
+
# @see .call
|
61
|
+
#
|
62
|
+
# @param message [Message] The message you want to amend.
|
63
|
+
# If none is provided, then one is created.
|
64
|
+
# @param [Hash] options
|
65
|
+
# @option options [Symbol, Array<Symbol>] :channels One or more channels that
|
66
|
+
# Harbinger will deliver the :message to
|
67
|
+
def deliver_message(message, options = {})
|
68
|
+
channels = options.fetch(:channels) { default_channels }
|
69
|
+
Array(channels).flatten.compact.each do |channel_name|
|
70
|
+
channel = channel_for(channel_name)
|
71
|
+
channel.deliver(message)
|
72
|
+
end
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def default_message
|
77
|
+
require 'harbinger/message'
|
78
|
+
Message.new
|
79
|
+
end
|
80
|
+
private_class_method :default_message
|
81
|
+
|
82
|
+
def reporter_for(context)
|
83
|
+
Reporters.find_for(context)
|
84
|
+
end
|
85
|
+
private_class_method :reporter_for
|
86
|
+
|
87
|
+
def channel_for(name)
|
88
|
+
Channels.find_for(name)
|
89
|
+
end
|
90
|
+
private_class_method :channel_for
|
91
|
+
|
92
|
+
def default_channels
|
93
|
+
configuration.default_channels
|
94
|
+
end
|
95
|
+
|
96
|
+
def logger
|
97
|
+
configuration.logger
|
98
|
+
end
|
99
|
+
|
100
|
+
def database_storage
|
101
|
+
configuration.database_storage
|
102
|
+
end
|
103
|
+
|
5
104
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Harbinger
|
2
|
+
module Channels
|
3
|
+
module_function
|
4
|
+
def find_for(channel_name)
|
5
|
+
channel_class_name = channel_name_for_instance(channel_name)
|
6
|
+
if const_defined?(channel_class_name)
|
7
|
+
const_get(channel_class_name)
|
8
|
+
else
|
9
|
+
NullChannel
|
10
|
+
end
|
11
|
+
rescue StandardError
|
12
|
+
NullChannel
|
13
|
+
end
|
14
|
+
|
15
|
+
def channel_name_for_instance(channel_name)
|
16
|
+
channel_name.to_s.gsub(/(?:^|_)([a-z])/) { $1.upcase } + "Channel"
|
17
|
+
end
|
18
|
+
private_class_method :channel_name_for_instance
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Note: I am not requiring all of the channels for this Gem. Some
|
23
|
+
# implementations my want to override the defaults. Look to
|
24
|
+
# `lib/harbinger/engine.rb` for how things are required for Rails applications.
|
25
|
+
require 'harbinger/channels/null_channel'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Harbinger::Channels
|
2
|
+
module DatabaseChannel
|
3
|
+
module_function
|
4
|
+
def deliver(message, options = {})
|
5
|
+
storage = options.fetch(:storage) { default_storage }
|
6
|
+
storage.store_message(message)
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_storage
|
10
|
+
Harbinger.database_storage
|
11
|
+
end
|
12
|
+
private_class_method :default_storage
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Harbinger::Channels
|
2
|
+
module LoggerChannel
|
3
|
+
module_function
|
4
|
+
def deliver(message, options = {})
|
5
|
+
logger = options.fetch(:logger) { default_logger }
|
6
|
+
severity = options.fetch(:severity) { default_severity }
|
7
|
+
read(message) do |line|
|
8
|
+
logger.add(severity, line)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(message)
|
13
|
+
yield("BEGIN MESSAGE OBJECT ID=#{message.object_id}")
|
14
|
+
message.attributes.each do |key, value|
|
15
|
+
yield("#{key.inspect} => #{value.inspect}")
|
16
|
+
end
|
17
|
+
yield("END MESSAGE OBJECT ID=#{message.object_id}")
|
18
|
+
end
|
19
|
+
private_class_method :read
|
20
|
+
|
21
|
+
def default_logger
|
22
|
+
Harbinger.logger
|
23
|
+
end
|
24
|
+
private_class_method :default_logger
|
25
|
+
|
26
|
+
def default_severity
|
27
|
+
5 # ::Logger::UNKNOWN
|
28
|
+
end
|
29
|
+
private_class_method :default_severity
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'harbinger/exceptions'
|
2
|
+
module Harbinger
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
def default_channels
|
6
|
+
@default_channels || __default_channels
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_channels=(*channel_names)
|
10
|
+
@default_channels = Array(channel_names).flatten.compact.collect{|name|
|
11
|
+
word = name.to_s
|
12
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
13
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
14
|
+
word.tr!("-", "_")
|
15
|
+
word.downcase!
|
16
|
+
word.to_sym
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger
|
21
|
+
@logger || default_logger
|
22
|
+
end
|
23
|
+
|
24
|
+
def logger=(object)
|
25
|
+
raise ConfigurationError.new("Expected Harbinger.database_storage to respond_to #add. #{object.inspect} does not respond to #add") unless object.respond_to?(:add)
|
26
|
+
@logger = object
|
27
|
+
end
|
28
|
+
|
29
|
+
def database_storage
|
30
|
+
@database_storage || default_database_storage
|
31
|
+
end
|
32
|
+
|
33
|
+
def database_storage=(object)
|
34
|
+
raise ConfigurationError.new("Expected Harbinger.database_storage to respond_to #store_message. #{object.inspect} does not respond to #add") unless object.respond_to?(:store_message)
|
35
|
+
@database_storage = object
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def default_logger
|
41
|
+
if defined?(Rails)
|
42
|
+
Rails.logger
|
43
|
+
else
|
44
|
+
require 'logger'
|
45
|
+
::Logger.new(STDOUT)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def default_database_storage
|
50
|
+
require 'harbinger/database_channel_message'
|
51
|
+
DatabaseChannelMessage
|
52
|
+
end
|
53
|
+
|
54
|
+
def __default_channels
|
55
|
+
[:logger]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/harbinger/engine.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
module Harbinger
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
-
|
3
|
+
engine_name 'harbinger'
|
4
|
+
|
5
|
+
config.to_prepare do
|
6
|
+
# Because I don't want to auto-require all of the dependent channels
|
7
|
+
Harbinger.default_channels.each do |channel_name|
|
8
|
+
require "harbinger/channels/#{channel_name}_channel"
|
9
|
+
end
|
10
|
+
end
|
4
11
|
end
|
5
12
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Harbinger
|
2
|
+
class Message
|
3
|
+
attr_reader :attributes
|
4
|
+
def initialize
|
5
|
+
@attributes = {}
|
6
|
+
yield(self) if block_given?
|
7
|
+
end
|
8
|
+
|
9
|
+
def append(container, key, value)
|
10
|
+
composite_key = "#{container}.#{key}"
|
11
|
+
@attributes[composite_key] ||= []
|
12
|
+
@attributes[composite_key] << value
|
13
|
+
end
|
14
|
+
|
15
|
+
def contexts
|
16
|
+
attributes.keys.collect { |key| key.split('.')[0] }.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Harbinger
|
2
|
+
module Reporters
|
3
|
+
module_function
|
4
|
+
def find_for(context)
|
5
|
+
if context.respond_to?(:to_harbinger_reporter)
|
6
|
+
context.to_harbinger_reporter
|
7
|
+
else
|
8
|
+
# @TODO - Handle inheritence; KeyError is not an Exception
|
9
|
+
reporter_class_name = reporter_name_for_instance(context)
|
10
|
+
if const_defined?(reporter_class_name)
|
11
|
+
const_get(reporter_class_name).new(context)
|
12
|
+
else
|
13
|
+
NullReporter.new(context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
rescue StandardError
|
17
|
+
NullReporter.new(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def reporter_name_for_instance(context)
|
21
|
+
if context.is_a?(Exception)
|
22
|
+
"ExceptionReporter"
|
23
|
+
else
|
24
|
+
context.class.to_s.gsub(/(?:^|_)([a-z])/) { $1.upcase } + "Reporter"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private_class_method :reporter_name_for_instance
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require "harbinger/reporters/user_reporter"
|
32
|
+
require "harbinger/reporters/request_reporter"
|
33
|
+
require "harbinger/reporters/exception_reporter"
|
34
|
+
require "harbinger/reporters/null_reporter"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Harbinger::Reporters
|
2
|
+
class ExceptionReporter
|
3
|
+
attr_reader :exception
|
4
|
+
|
5
|
+
def initialize(exception)
|
6
|
+
@exception = exception
|
7
|
+
end
|
8
|
+
|
9
|
+
def accept(message)
|
10
|
+
|
11
|
+
if exception.respond_to?(:class)
|
12
|
+
message.append('exception', 'class_name', exception.class.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
if exception.respond_to?(:backtrace)
|
16
|
+
message.append('exception', 'backtrace', Array(exception.backtrace).join("\n"))
|
17
|
+
end
|
18
|
+
|
19
|
+
if exception.respond_to?(:message)
|
20
|
+
message.append('exception', 'message', exception.message)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Harbinger
|
2
|
+
module Reporters
|
3
|
+
class RequestReporter
|
4
|
+
attr_reader :request, :method_names
|
5
|
+
private :method_names
|
6
|
+
def initialize(request, config = {})
|
7
|
+
@request = request
|
8
|
+
@method_names = config.fetch(:method_names) { ['path', 'params', 'user_agent'] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def accept(message)
|
12
|
+
method_names.each do |method_name|
|
13
|
+
if request.respond_to?(method_name)
|
14
|
+
message.append('request', method_name, request.public_send(method_name))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Harbinger::Reporters
|
2
|
+
class UserReporter
|
3
|
+
attr_reader :user, :method_names
|
4
|
+
private :method_names
|
5
|
+
|
6
|
+
def initialize(user, config = {})
|
7
|
+
@user = user
|
8
|
+
@method_names = config.fetch(:method_names) { ['username'] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def accept(message)
|
12
|
+
method_names.each do |method_name|
|
13
|
+
if user.respond_to?(method_name)
|
14
|
+
message.append('user', method_name, user.public_send(method_name))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/harbinger/version.rb
CHANGED
data/script/fast_specs
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
module Fast
|
6
|
+
module_function
|
7
|
+
def spec?(fn)
|
8
|
+
open(fn) { |f| f.gets =~ /fast_helper/ }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
fast_specs = FileList['spec/**/*_spec.rb'].select { |fn|
|
13
|
+
Fast.spec?(fn)
|
14
|
+
}
|
15
|
+
if fast_specs.any?
|
16
|
+
system "rspec #{fast_specs}"
|
17
|
+
else
|
18
|
+
puts "Unable to find any fast specs"
|
19
|
+
exit(-1)
|
20
|
+
end
|