wonkavision 0.5.11 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +3 -0
- data/lib/wonkavision.rb +28 -1
- data/lib/wonkavision/aggregation.rb +21 -0
- data/lib/wonkavision/event_coordinator.rb +19 -7
- data/lib/wonkavision/extensions/symbol.rb +55 -0
- data/lib/wonkavision/facts.rb +27 -0
- data/lib/wonkavision/local_job_queue.rb +28 -0
- data/lib/wonkavision/message_mapper.rb +2 -2
- data/lib/wonkavision/message_mapper/map.rb +60 -8
- data/lib/wonkavision/persistence/mongo.rb +95 -0
- data/lib/wonkavision/plugins.rb +2 -1
- data/lib/wonkavision/plugins/analytics/aggregation.rb +139 -0
- data/lib/wonkavision/plugins/analytics/aggregation/aggregation_spec.rb +53 -0
- data/lib/wonkavision/plugins/analytics/aggregation/attribute.rb +22 -0
- data/lib/wonkavision/plugins/analytics/aggregation/dimension.rb +64 -0
- data/lib/wonkavision/plugins/analytics/aggregation/measure.rb +240 -0
- data/lib/wonkavision/plugins/analytics/cellset.rb +171 -0
- data/lib/wonkavision/plugins/analytics/facts.rb +106 -0
- data/lib/wonkavision/plugins/analytics/handlers/apply_aggregation.rb +35 -0
- data/lib/wonkavision/plugins/analytics/handlers/split_by_aggregation.rb +60 -0
- data/lib/wonkavision/plugins/analytics/member_filter.rb +106 -0
- data/lib/wonkavision/plugins/analytics/mongo.rb +6 -0
- data/lib/wonkavision/plugins/analytics/persistence/hash_store.rb +59 -0
- data/lib/wonkavision/plugins/analytics/persistence/mongo_store.rb +85 -0
- data/lib/wonkavision/plugins/analytics/persistence/store.rb +105 -0
- data/lib/wonkavision/plugins/analytics/query.rb +76 -0
- data/lib/wonkavision/plugins/event_handling.rb +15 -3
- data/lib/wonkavision/version.rb +1 -1
- data/test/aggregation_spec_test.rb +99 -0
- data/test/aggregation_test.rb +170 -0
- data/test/analytics/test_aggregation.rb +78 -0
- data/test/apply_aggregation_test.rb +92 -0
- data/test/attribute_test.rb +26 -0
- data/test/cellset_test.rb +200 -0
- data/test/dimension_test.rb +186 -0
- data/test/facts_test.rb +146 -0
- data/test/hash_store_test.rb +112 -0
- data/test/log/test.log +96844 -0
- data/test/map_test.rb +48 -1
- data/test/measure_test.rb +146 -0
- data/test/member_filter_test.rb +143 -0
- data/test/mongo_store_test.rb +115 -0
- data/test/query_test.rb +106 -0
- data/test/split_by_aggregation_test.rb +114 -0
- data/test/store_test.rb +71 -0
- data/test/symbol_test.rb +62 -0
- data/test/test_activity_models.rb +1 -1
- data/test/test_aggregation.rb +42 -0
- data/test/test_data.tuples +100 -0
- data/test/test_helper.rb +7 -0
- metadata +57 -5
data/CHANGELOG.rdoc
CHANGED
data/lib/wonkavision.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "active_support"
|
3
3
|
require "active_support/hash_with_indifferent_access" unless defined?(HashWithIndifferentAccess)
|
4
|
+
require "active_support/core_ext"
|
4
5
|
|
5
6
|
dir = File.dirname(__FILE__)
|
6
7
|
["support",
|
@@ -9,9 +10,12 @@ dir = File.dirname(__FILE__)
|
|
9
10
|
"event",
|
10
11
|
"event_context",
|
11
12
|
"event_namespace",
|
13
|
+
"local_job_queue",
|
12
14
|
"event_coordinator",
|
13
15
|
"event_binding",
|
14
16
|
"event_handler",
|
17
|
+
"facts",
|
18
|
+
"aggregation",
|
15
19
|
"message_mapper/indifferent_access",
|
16
20
|
"message_mapper/map",
|
17
21
|
"message_mapper",
|
@@ -20,14 +24,33 @@ dir = File.dirname(__FILE__)
|
|
20
24
|
"plugins/business_activity/event_binding",
|
21
25
|
"plugins/business_activity",
|
22
26
|
"plugins/timeline",
|
27
|
+
"extensions/symbol",
|
28
|
+
"plugins/analytics/member_filter",
|
29
|
+
"plugins/analytics/persistence/store",
|
30
|
+
"plugins/analytics/persistence/hash_store",
|
31
|
+
"plugins/analytics/facts",
|
32
|
+
"plugins/analytics/aggregation/aggregation_spec",
|
33
|
+
"plugins/analytics/aggregation/attribute",
|
34
|
+
"plugins/analytics/aggregation/dimension",
|
35
|
+
"plugins/analytics/aggregation/measure",
|
36
|
+
"plugins/analytics/aggregation",
|
37
|
+
"plugins/analytics/cellset",
|
38
|
+
"plugins/analytics/query",
|
23
39
|
"acts_as_oompa_loompa",
|
24
40
|
"persistence/mongo_mapper_adapter",
|
25
|
-
"persistence/mongoid_adapter"
|
41
|
+
"persistence/mongoid_adapter",
|
42
|
+
"plugins/analytics/mongo"
|
43
|
+
].each {|lib|require File.join(dir,'wonkavision',lib)}
|
44
|
+
|
45
|
+
|
46
|
+
|
26
47
|
|
27
48
|
#require File.join(dir,"cubicle","mongo_mapper","aggregate_plugin") if defined?(MongoMapper::Document)
|
28
49
|
|
29
50
|
module Wonkavision
|
30
51
|
|
52
|
+
NaN = 0.0 / 0.0
|
53
|
+
|
31
54
|
# def self.register_cubicle_directory(directory_path, recursive=true)
|
32
55
|
# searcher = "#{recursive ? "*" : "**/*"}.rb"
|
33
56
|
# Dir[File.join(directory_path,searcher)].each {|cubicle| require cubicle}
|
@@ -73,3 +96,7 @@ module Wonkavision
|
|
73
96
|
|
74
97
|
|
75
98
|
end
|
99
|
+
|
100
|
+
#Load event handlers for analytics
|
101
|
+
# dir = File.dirname(__FILE__)
|
102
|
+
Dir[File.join(dir,"wonkavision","plugins/analytics/handlers/**/*.rb")].each {|lib|require lib}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Aggregation
|
3
|
+
|
4
|
+
def self.all
|
5
|
+
Wonkavision::Plugins::Aggregation.all
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.persistence
|
9
|
+
@persistence
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(handler)
|
13
|
+
handler.class_eval do
|
14
|
+
extend Plugins
|
15
|
+
use Plugins::Aggregation
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,11 +2,13 @@ module Wonkavision
|
|
2
2
|
class EventCoordinator
|
3
3
|
|
4
4
|
attr_reader :root_namespace
|
5
|
+
attr_accessor :broadcast_transport, :job_queue
|
5
6
|
|
6
7
|
def initialize
|
7
8
|
@root_namespace = Wonkavision::EventNamespace.new
|
8
|
-
|
9
|
+
#@lock = Mutex.new
|
9
10
|
#@event_cache = {}
|
11
|
+
|
10
12
|
@incoming_event_filters = []
|
11
13
|
end
|
12
14
|
|
@@ -22,7 +24,7 @@ module Wonkavision
|
|
22
24
|
self.instance_eval(&block)
|
23
25
|
end
|
24
26
|
|
25
|
-
def map
|
27
|
+
def map
|
26
28
|
yield root_namespace if block_given?
|
27
29
|
end
|
28
30
|
|
@@ -33,17 +35,27 @@ module Wonkavision
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def receive_event(event_path, event_data)
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
#@lock.synchronize do
|
39
|
+
#If process_incoming_event returns nil or false, it means a filter chose to abort
|
40
|
+
#the event processing, in which case we'll break for lunch.
|
39
41
|
return unless event_data = process_incoming_event(event_path,event_data)
|
40
42
|
|
41
43
|
event_path = Wonkavision.normalize_event_path(event_path)
|
42
44
|
targets = root_namespace.find_matching_segments(event_path).values
|
43
|
-
#If the event wasn't matched, maybe someone is subscribing to '/*' ?
|
45
|
+
#If the event wasn't matched, maybe someone is subscribing to '/*' ?
|
44
46
|
targets = [root_namespace] if targets.blank?
|
45
47
|
targets.each{|target|target.notify_subscribers(event_data,event_path)}
|
46
|
-
end
|
48
|
+
#end
|
49
|
+
end
|
50
|
+
|
51
|
+
def publish(event_path, event_data)
|
52
|
+
raise "No transport was configured with the EventCoordinator to deliver broadcast messages. Please set Wonkavision.event_coordinator.broadcast_transport = <some transport>." unless broadcast_transport
|
53
|
+
broadcast_transport.publish(event_path, event_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def submit_job(event_path, event_data)
|
57
|
+
job_queue ? job_queue.publish(event_path,event_data) :
|
58
|
+
receive_event(event_path, event_data)
|
47
59
|
end
|
48
60
|
|
49
61
|
protected
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# This concept is torn from the chest cavity of
|
3
|
+
# jnunemakers plucky library (https://github.com/jnunemaker/plucky/blob/master/lib/plucky/extensions/symbol.rb)
|
4
|
+
module Wonkavision
|
5
|
+
module Extensions
|
6
|
+
module Symbol
|
7
|
+
|
8
|
+
[:key, :caption, :sort].each do |dimension_attribute|
|
9
|
+
define_method(dimension_attribute) do
|
10
|
+
_filter(dimension_attribute, :member_type=>:dimension)
|
11
|
+
end unless method_defined?(dimension_attribute)
|
12
|
+
end
|
13
|
+
|
14
|
+
[:sum, :sum2, :count].each do |measure_attribute|
|
15
|
+
define_method(measure_attribute) do
|
16
|
+
_filter(measure_attribute, :member_type=>:measure)
|
17
|
+
end unless method_defined?(measure_attribute)
|
18
|
+
end
|
19
|
+
|
20
|
+
def[](name)
|
21
|
+
_filter(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(name,*args)
|
25
|
+
_filter(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def _member_type
|
30
|
+
self == :measures ? :measure : :dimension
|
31
|
+
end
|
32
|
+
|
33
|
+
def _is_member_name?
|
34
|
+
[:dimensions,:measures].include?(self) == false
|
35
|
+
end
|
36
|
+
|
37
|
+
def _filter(name, options={})
|
38
|
+
options[:member_type] ||= _member_type
|
39
|
+
if _is_member_name?
|
40
|
+
member_name = self
|
41
|
+
options[:attribute_name] = name
|
42
|
+
else
|
43
|
+
member_name = name
|
44
|
+
end
|
45
|
+
Wonkavision::Analytics::MemberFilter.new(member_name,options)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
class Symbol
|
54
|
+
include Wonkavision::Extensions::Symbol
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Facts
|
3
|
+
|
4
|
+
def self.persistence
|
5
|
+
@persistence
|
6
|
+
end
|
7
|
+
|
8
|
+
#current only supports :mongo
|
9
|
+
def self.persistence=(backend)
|
10
|
+
case backend
|
11
|
+
when :mongo then require File.dirname(__FILE__) + "/plugins/analytics/mongo"
|
12
|
+
else
|
13
|
+
raise "#{backend} is not a supported back end for Wonkavision analytics"
|
14
|
+
end
|
15
|
+
@persistence = backend
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(facts)
|
19
|
+
facts.class_eval do
|
20
|
+
extend Plugins
|
21
|
+
use Plugins::EventHandling
|
22
|
+
use Plugins::Callbacks
|
23
|
+
use Plugins::Facts
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Wonkavision
|
4
|
+
class LocalJobQueue
|
5
|
+
attr_reader :queue
|
6
|
+
def initialize(options={})
|
7
|
+
worker_count = options[:workers] || 2
|
8
|
+
@queue = Queue.new
|
9
|
+
@workers = []
|
10
|
+
worker_count.times do
|
11
|
+
Thread.new do
|
12
|
+
while true
|
13
|
+
if msg = @queue.pop
|
14
|
+
Wonkavision.event_coordinator.receive_event(msg[0],msg[1])
|
15
|
+
else
|
16
|
+
sleep 0.1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish(event_path,event)
|
24
|
+
@queue << [event_path, event]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -11,11 +11,11 @@ module Wonkavision
|
|
11
11
|
MessageMapper.maps[map_name] = block
|
12
12
|
end
|
13
13
|
|
14
|
-
def execute(map,data_source)
|
14
|
+
def execute(map,data_source,options={})
|
15
15
|
map_block = map.kind_of?(Proc) ? map : MessageMapper.maps[map]
|
16
16
|
|
17
17
|
raise "#{map} not found" unless map_block
|
18
|
-
MessageMapper::Map.new.execute(data_source, map_block)
|
18
|
+
MessageMapper::Map.new.execute(data_source, map_block, options)
|
19
19
|
end
|
20
20
|
|
21
21
|
def register_map_directory(directory_path, recursive=true)
|
@@ -5,12 +5,14 @@ module Wonkavision
|
|
5
5
|
include IndifferentAccess
|
6
6
|
|
7
7
|
def initialize(context = nil)
|
8
|
+
@write_nils = true
|
8
9
|
@context_stack = []
|
9
10
|
@context_stack.push(context) if context
|
10
11
|
@formats = default_formats
|
11
12
|
end
|
12
13
|
|
13
|
-
def execute(context,map_block)
|
14
|
+
def execute(context,map_block,options={})
|
15
|
+
@write_nils = options[:write_nils].nil? ? true : options[:write_nils]
|
14
16
|
@context_stack.push(context)
|
15
17
|
instance_eval(&map_block)
|
16
18
|
@context_stack.clear
|
@@ -25,6 +27,14 @@ module Wonkavision
|
|
25
27
|
@context_stack[-1]
|
26
28
|
end
|
27
29
|
|
30
|
+
def ignore_nil!
|
31
|
+
@write_nils = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_nil!
|
35
|
+
@write_nils = true
|
36
|
+
end
|
37
|
+
|
28
38
|
def from (context,&block)
|
29
39
|
raise "No block ws provided to 'from'" unless block
|
30
40
|
return if context.nil?
|
@@ -146,7 +156,7 @@ module Wonkavision
|
|
146
156
|
if kind_of?(Date)
|
147
157
|
self
|
148
158
|
elsif respond_to?(:to_date)
|
149
|
-
|
159
|
+
to_date
|
150
160
|
elsif (date_str=to_s) && date_str.length > 0
|
151
161
|
begin
|
152
162
|
Date.parse(date_str)
|
@@ -187,7 +197,45 @@ module Wonkavision
|
|
187
197
|
end
|
188
198
|
end
|
189
199
|
|
200
|
+
def duration(*args, &block)
|
201
|
+
opts = args.extract_options! || {}
|
202
|
+
|
203
|
+
from = opts.delete(:from)
|
204
|
+
to = opts.delete(:to)
|
205
|
+
|
206
|
+
return nil unless from || to
|
207
|
+
|
208
|
+
from ||= Time.now; to ||= Time.now
|
209
|
+
|
210
|
+
unit = opts.delete(:in) || opts.delete(:unit) || :seconds
|
211
|
+
|
212
|
+
duration = convert_seconds(to-from,unit)
|
213
|
+
|
214
|
+
assignment = {args.shift => duration}
|
215
|
+
args << assignment << opts
|
216
|
+
|
217
|
+
value(*args, &block)
|
218
|
+
|
219
|
+
end
|
220
|
+
alias :elapsed :duration
|
221
|
+
|
190
222
|
private
|
223
|
+
|
224
|
+
def convert_seconds(duration, unit)
|
225
|
+
duration /
|
226
|
+
case unit.to_s
|
227
|
+
when "seconds" then 1
|
228
|
+
when "minutes" then 60
|
229
|
+
when "hours" then 60 * 60
|
230
|
+
when "days" then 60 * 60 * 24
|
231
|
+
when "weeks" then 60 * 60 * 24 * 7
|
232
|
+
when "months" then 60 * 60 * 24 * 30
|
233
|
+
when "years" then 60 * 60 * 24 * 365
|
234
|
+
else raise "Cannot convert duration to unknown time unit #{unit}"
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
191
239
|
def format_value(val,opts={})
|
192
240
|
val = opts[:default] || opts[:default_value] if val.nil?
|
193
241
|
return val if val.nil?
|
@@ -202,16 +250,18 @@ module Wonkavision
|
|
202
250
|
end
|
203
251
|
|
204
252
|
def extract_value_from_context(context,field_name,block=nil)
|
205
|
-
|
206
|
-
|
207
|
-
elsif context.respond_to?(:[])
|
253
|
+
value = nil
|
254
|
+
if context.respond_to?(:[])
|
208
255
|
value = context[field_name]
|
209
256
|
if value.nil? && field_name
|
210
257
|
value = context[field_name.to_sym] || context[field_name.to_s]
|
211
258
|
end
|
212
|
-
else
|
213
|
-
value = nil
|
214
259
|
end
|
260
|
+
|
261
|
+
if context.respond_to?(field_name.to_sym)
|
262
|
+
value = context.instance_eval("self.#{field_name}")
|
263
|
+
end unless value
|
264
|
+
|
215
265
|
value = value.instance_eval(&block) if block
|
216
266
|
value
|
217
267
|
end
|
@@ -219,7 +269,9 @@ module Wonkavision
|
|
219
269
|
def set_value(field_name,val,opts={})
|
220
270
|
if prefix = opts[:prefix]; field_name = "#{prefix}#{field_name}"; end
|
221
271
|
if suffix = opts[:suffix]; field_name = "#{field_name}#{suffix}"; end
|
222
|
-
|
272
|
+
unless val.nil? && !@write_nils
|
273
|
+
self[field_name] = format_value(val,opts)
|
274
|
+
end
|
223
275
|
end
|
224
276
|
|
225
277
|
def default_formats
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Taken almost verbatim from https://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper/connection.rb
|
3
|
+
require 'uri'
|
4
|
+
require 'mongo'
|
5
|
+
|
6
|
+
module Wonkavision
|
7
|
+
module Mongo
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def initialize(connection=nil)
|
11
|
+
@connection = connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
def connection
|
16
|
+
@connection ||= ::Mongo::Connection.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
def connection=(new_connection)
|
21
|
+
@connection = new_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
def logger
|
26
|
+
connection.logger
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api public
|
30
|
+
def database=(name)
|
31
|
+
@database = nil
|
32
|
+
@database_name = name
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api public
|
36
|
+
def database
|
37
|
+
if @database_name.blank?
|
38
|
+
raise 'You forgot to set the default database name: database = "foobar"'
|
39
|
+
end
|
40
|
+
|
41
|
+
@database ||= connection.db(@database_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def config=(hash)
|
45
|
+
@config = hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def config
|
49
|
+
raise 'Set config before connecting. config = {...}' unless defined?(@config)
|
50
|
+
@config
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def config_for_environment(environment)
|
55
|
+
env = config[environment]
|
56
|
+
return env if env['uri'].blank?
|
57
|
+
|
58
|
+
uri = URI.parse(env['uri'])
|
59
|
+
raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
|
60
|
+
{
|
61
|
+
'host' => uri.host,
|
62
|
+
'port' => uri.port,
|
63
|
+
'database' => uri.path.gsub(/^\//, ''),
|
64
|
+
'username' => uri.user,
|
65
|
+
'password' => uri.password,
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def connect(environment, options={})
|
70
|
+
raise 'Set config before connecting. config = {...}' if config.blank?
|
71
|
+
env = config_for_environment(environment)
|
72
|
+
self.connection = ::Mongo::Connection.new(env['host'], env['port'], options)
|
73
|
+
self.database = env['database']
|
74
|
+
database.authenticate(env['username'], env['password']) if env['username'] && env['password']
|
75
|
+
end
|
76
|
+
|
77
|
+
def setup(config, environment, options={})
|
78
|
+
handle_passenger_forking
|
79
|
+
self.config = config
|
80
|
+
connect(environment, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_passenger_forking
|
84
|
+
if defined?(PhusionPassenger)
|
85
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
86
|
+
connection.connect if forked
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Connection
|
92
|
+
include Wonkavision::Mongo
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|