dorothy2 0.0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +30 -6
- data/TODO +21 -0
- data/bin/dorothy_start +7 -6
- data/bin/dparser_start +13 -1
- data/etc/ddl/dorothive.ddl +2 -31
- data/lib/doroParser.rb +30 -23
- data/lib/dorothy2/BFM.rb +5 -13
- data/lib/dorothy2/do-utils.rb +4 -5
- data/lib/dorothy2/version.rb +1 -1
- data/lib/dorothy2.rb +5 -8
- data/lib/mu/xtractr/about.rb +57 -0
- data/lib/mu/xtractr/content.rb +68 -0
- data/lib/mu/xtractr/field.rb +178 -0
- data/lib/mu/xtractr/flow.rb +162 -0
- data/lib/mu/xtractr/flows.rb +118 -0
- data/lib/mu/xtractr/host.rb +87 -0
- data/lib/mu/xtractr/packet.rb +138 -0
- data/lib/mu/xtractr/packets.rb +122 -0
- data/lib/mu/xtractr/service.rb +77 -0
- data/lib/mu/xtractr/stream/http.rb +103 -0
- data/lib/mu/xtractr/stream.rb +132 -0
- data/lib/mu/xtractr/term.rb +73 -0
- data/lib/mu/xtractr/test/stream/tc_http.rb +53 -0
- data/lib/mu/xtractr/test/tc_field.rb +140 -0
- data/lib/mu/xtractr/test/tc_flow.rb +79 -0
- data/lib/mu/xtractr/test/tc_flows.rb +94 -0
- data/lib/mu/xtractr/test/tc_host.rb +116 -0
- data/lib/mu/xtractr/test/tc_packet.rb +110 -0
- data/lib/mu/xtractr/test/tc_packets.rb +84 -0
- data/lib/mu/xtractr/test/tc_service.rb +66 -0
- data/lib/mu/xtractr/test/tc_stream.rb +56 -0
- data/lib/mu/xtractr/test/tc_term.rb +59 -0
- data/lib/mu/xtractr/test/tc_views.rb +118 -0
- data/lib/mu/xtractr/test/tc_xtractr.rb +151 -0
- data/lib/mu/xtractr/test/test.rb +19 -0
- data/lib/mu/xtractr/views.rb +204 -0
- data/lib/mu/xtractr.rb +257 -0
- metadata +32 -4
@@ -0,0 +1,118 @@
|
|
1
|
+
# "THE BEER-WARE LICENSE" (Revision 42):
|
2
|
+
# Mu[http://www.mudynamics.com] wrote this file. As long as you retain this
|
3
|
+
# notice you can do whatever you want with this stuff. If we meet some day,
|
4
|
+
# and you think this stuff is worth it, you can buy us a beer in return.
|
5
|
+
#
|
6
|
+
# All about pcapr
|
7
|
+
# * http://www.pcapr.net
|
8
|
+
# * http://groups.google.com/group/pcapr-forum
|
9
|
+
# * http://twitter.com/pcapr
|
10
|
+
#
|
11
|
+
# Mu Dynamics
|
12
|
+
# * http://www.mudynamics.com
|
13
|
+
# * http://labs.mudynamics.com
|
14
|
+
|
15
|
+
require 'mu/xtractr'
|
16
|
+
require 'test/unit'
|
17
|
+
|
18
|
+
module Mu
|
19
|
+
class Xtractr
|
20
|
+
class Views
|
21
|
+
class Count
|
22
|
+
class Test < Test::Unit::TestCase
|
23
|
+
attr_reader :xtractr
|
24
|
+
attr_reader :count
|
25
|
+
|
26
|
+
def setup
|
27
|
+
@xtractr = Xtractr.new
|
28
|
+
@count = xtractr.flows('flow.service:DNS').count('dns.qry.name').first
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_attributes
|
32
|
+
assert_kind_of(Field, count.field)
|
33
|
+
assert_equal('ax.search.itunes.apple.com', count.value)
|
34
|
+
assert_equal(8, count.count)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_object
|
38
|
+
object = count.object
|
39
|
+
assert_kind_of(Field::Value, object)
|
40
|
+
assert_equal('dns.qry.name', object.field.name)
|
41
|
+
assert_equal('ax.search.itunes.apple.com', object.value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_packets
|
45
|
+
packets = count.packets
|
46
|
+
assert_equal("dns.qry.name:\"ax.search.itunes.apple.com\"", packets.q)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_each_packet
|
50
|
+
count.each_packet do |pkt|
|
51
|
+
assert_kind_of(Packet, pkt)
|
52
|
+
values = pkt['dns.qry.name']
|
53
|
+
assert_equal(1, values.size)
|
54
|
+
assert_equal('ax.search.itunes.apple.com', values[0])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_sum
|
59
|
+
sums = count.object.sum('pkt.src', 'pkt.length')
|
60
|
+
assert_equal(2, sums.length)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_inspect
|
64
|
+
assert_nothing_raised { count.inspect }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end # Count
|
68
|
+
|
69
|
+
class Sum
|
70
|
+
class Test < Test::Unit::TestCase
|
71
|
+
attr_reader :xtractr
|
72
|
+
attr_reader :sum
|
73
|
+
|
74
|
+
def setup
|
75
|
+
@xtractr = Xtractr.new
|
76
|
+
@sum = xtractr.flows('flow.service:DNS').sum('dns.qry.name', 'flow.bytes').first
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_attributes
|
80
|
+
assert_kind_of(Field, sum.field)
|
81
|
+
assert_equal('ax.search.itunes.apple.com', sum.value)
|
82
|
+
assert_equal(1220, sum.sum)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_object
|
86
|
+
object = sum.object
|
87
|
+
assert_kind_of(Field::Value, object)
|
88
|
+
assert_equal('dns.qry.name', object.field.name)
|
89
|
+
assert_equal('ax.search.itunes.apple.com', object.value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_packets
|
93
|
+
packets = sum.packets
|
94
|
+
assert_equal("dns.qry.name:\"ax.search.itunes.apple.com\"", packets.q)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_each_packet
|
98
|
+
sum.each_packet do |pkt|
|
99
|
+
assert_kind_of(Packet, pkt)
|
100
|
+
values = pkt['dns.qry.name']
|
101
|
+
assert_equal(1, values.size)
|
102
|
+
assert_equal('ax.search.itunes.apple.com', values[0])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_count
|
107
|
+
counts = sum.object.count('pkt.service')
|
108
|
+
assert_equal(1, counts.length)
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_inspect
|
112
|
+
assert_nothing_raised { sum.inspect }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end # Sum
|
116
|
+
end # Views
|
117
|
+
end # Xtractr
|
118
|
+
end # Mu
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# "THE BEER-WARE LICENSE" (Revision 42):
|
2
|
+
# Mu[http://www.mudynamics.com] wrote this file. As long as you retain this
|
3
|
+
# notice you can do whatever you want with this stuff. If we meet some day,
|
4
|
+
# and you think this stuff is worth it, you can buy us a beer in return.
|
5
|
+
#
|
6
|
+
# All about pcapr
|
7
|
+
# * http://www.pcapr.net
|
8
|
+
# * http://groups.google.com/group/pcapr-forum
|
9
|
+
# * http://twitter.com/pcapr
|
10
|
+
#
|
11
|
+
# Mu Dynamics
|
12
|
+
# * http://www.mudynamics.com
|
13
|
+
# * http://labs.mudynamics.com
|
14
|
+
|
15
|
+
require 'mu/xtractr'
|
16
|
+
require 'test/unit'
|
17
|
+
|
18
|
+
module Mu
|
19
|
+
class Xtractr
|
20
|
+
class Test < Test::Unit::TestCase
|
21
|
+
attr_reader :xtractr
|
22
|
+
|
23
|
+
def setup
|
24
|
+
@xtractr = Xtractr.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_about
|
28
|
+
about = xtractr.about
|
29
|
+
assert_equal(1778, about.packets)
|
30
|
+
assert_equal(32, about.flows)
|
31
|
+
assert_equal(12, about.hosts)
|
32
|
+
assert_equal(5, about.services)
|
33
|
+
assert_equal(171.172, about.duration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_hosts
|
37
|
+
hosts = xtractr.hosts
|
38
|
+
assert_equal(12, hosts.size)
|
39
|
+
hosts.each { |host| assert_instance_of(Host, host) }
|
40
|
+
assert_equal(8, xtractr.hosts(/^8.18/).size)
|
41
|
+
assert_equal(8, xtractr.hosts('8.18').size)
|
42
|
+
assert_equal(1, xtractr.hosts(/^4.2/).size)
|
43
|
+
assert_equal(1, xtractr.hosts('4.2').size)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_host
|
47
|
+
[
|
48
|
+
'4.2.2.1',
|
49
|
+
'8.18.65.67',
|
50
|
+
'8.18.65.32',
|
51
|
+
'8.18.65.58',
|
52
|
+
'8.18.65.82',
|
53
|
+
'8.18.65.27',
|
54
|
+
'8.18.65.10',
|
55
|
+
'8.18.65.88',
|
56
|
+
'8.18.65.89',
|
57
|
+
'66.235.132.121',
|
58
|
+
'192.168.1.10',
|
59
|
+
'224.0.0.251',
|
60
|
+
].each do |address|
|
61
|
+
assert_nothing_raised { xtractr.host address }
|
62
|
+
end
|
63
|
+
|
64
|
+
assert_raise(ArgumentError) { xtractr.host '1.1.1.1' }
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_services
|
68
|
+
services = xtractr.services
|
69
|
+
assert_equal(5, services.size)
|
70
|
+
services.each { |service| assert_instance_of(Service, service) }
|
71
|
+
assert_equal(2, xtractr.services(/HTTP/).size)
|
72
|
+
assert_equal(2, xtractr.services('HTTP').size)
|
73
|
+
assert_equal(2, xtractr.services('http').size)
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_service
|
77
|
+
[ 'DNS', 'TCP', 'HTTP', 'HTTP/XML', 'MDNS' ].each do |name|
|
78
|
+
assert_nothing_raised { xtractr.service name }
|
79
|
+
assert_nothing_raised { xtractr.service name.downcase }
|
80
|
+
end
|
81
|
+
|
82
|
+
assert_raise(ArgumentError) { xtractr.service 'blah' }
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_fields
|
86
|
+
fields = xtractr.fields
|
87
|
+
assert_equal(170, fields.size)
|
88
|
+
fields.each { |field| assert_instance_of(Field, field) }
|
89
|
+
assert_equal(12, xtractr.fields(/^pkt\./).size)
|
90
|
+
assert_equal(12, xtractr.fields("PKT.").size)
|
91
|
+
assert_equal(12, xtractr.fields("pkt.").size)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_field
|
95
|
+
[
|
96
|
+
'pkt.src', 'pkt.dst', 'pkt.flow', 'pkt.id', 'pkt.pcap', 'pkt.first',
|
97
|
+
'pkt.dir', 'pkt.time', 'pkt.offset', 'pkt.length', 'pkt.service',
|
98
|
+
'pkt.title'
|
99
|
+
].each do |name|
|
100
|
+
assert_nothing_raised { xtractr.field name }
|
101
|
+
end
|
102
|
+
assert_raise(ArgumentError) { xtractr.field 'blah' }
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_flows
|
106
|
+
flows = xtractr.flows
|
107
|
+
assert_kind_of(Flows, flows)
|
108
|
+
assert_equal('*', flows.q)
|
109
|
+
|
110
|
+
flows = xtractr.flows 'blah:foo'
|
111
|
+
assert_equal('blah:foo', flows.q)
|
112
|
+
|
113
|
+
flows = xtractr.flows 1..10
|
114
|
+
assert_equal('flow.id:[1 10]', flows.q)
|
115
|
+
|
116
|
+
flows = xtractr.flows 1...10
|
117
|
+
assert_equal('flow.id:[1 9]', flows.q)
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_flow
|
121
|
+
flow = xtractr.flow 1
|
122
|
+
assert_kind_of(Flow, flow)
|
123
|
+
|
124
|
+
assert_raise(ArgumentError) do
|
125
|
+
flow = xtractr.flow xtractr.about.flows+1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_packets
|
130
|
+
packets = xtractr.packets
|
131
|
+
assert_kind_of(Packets, packets)
|
132
|
+
assert_equal('*', packets.q)
|
133
|
+
|
134
|
+
packets = xtractr.packets 'blah:foo'
|
135
|
+
assert_equal('blah:foo', packets.q)
|
136
|
+
|
137
|
+
packets = xtractr.packets 1..10
|
138
|
+
assert_equal('pkt.id:[1 10]', packets.q)
|
139
|
+
|
140
|
+
packets = xtractr.packets 1...10
|
141
|
+
assert_equal('pkt.id:[1 9]', packets.q)
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_packet
|
145
|
+
pkt = xtractr.packet 1
|
146
|
+
assert_kind_of(Packet, pkt)
|
147
|
+
assert_equal(1, pkt.id)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end # Xtractr
|
151
|
+
end # Mu
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# "THE BEER-WARE LICENSE" (Revision 42):
|
2
|
+
# Mu[http://www.mudynamics.com] wrote this file. As long as you retain this
|
3
|
+
# notice you can do whatever you want with this stuff. If we meet some day,
|
4
|
+
# and you think this stuff is worth it, you can buy us a beer in return.
|
5
|
+
#
|
6
|
+
# All about pcapr
|
7
|
+
# * http://www.pcapr.net
|
8
|
+
# * http://groups.google.com/group/pcapr-forum
|
9
|
+
# * http://twitter.com/pcapr
|
10
|
+
#
|
11
|
+
# Mu Dynamics
|
12
|
+
# * http://www.mudynamics.com
|
13
|
+
# * http://labs.mudynamics.com
|
14
|
+
|
15
|
+
require 'test/unit'
|
16
|
+
|
17
|
+
Dir.glob('mu/xtractr/test/**/tc*.rb').each do |file|
|
18
|
+
require file
|
19
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# "THE BEER-WARE LICENSE" (Revision 42):
|
2
|
+
# Mu[http://www.mudynamics.com] wrote this file. As long as you retain this
|
3
|
+
# notice you can do whatever you want with this stuff. If we meet some day,
|
4
|
+
# and you think this stuff is worth it, you can buy us a beer in return.
|
5
|
+
#
|
6
|
+
# All about pcapr
|
7
|
+
# * http://www.pcapr.net
|
8
|
+
# * http://groups.google.com/group/pcapr-forum
|
9
|
+
# * http://twitter.com/pcapr
|
10
|
+
#
|
11
|
+
# Mu Dynamics
|
12
|
+
# * http://www.mudynamics.com
|
13
|
+
# * http://labs.mudynamics.com
|
14
|
+
|
15
|
+
module Mu
|
16
|
+
class Xtractr
|
17
|
+
# See http://labs.mudynamics.com/2009/04/03/interactive-couchdb/ for a quick
|
18
|
+
# tutorial on how Map/Reduce works.
|
19
|
+
class Views # :nodoc:
|
20
|
+
# = Count
|
21
|
+
# Count contains the results of doing a map/reduce on either flows or
|
22
|
+
# packets. Each count contains the field on which the map/reduce was
|
23
|
+
# performed, the unique value as all as the count of that value in the
|
24
|
+
# flows or packets. For example to count the unique source IP address of
|
25
|
+
# HTTP flows in the first five minutes of the index, you would do:
|
26
|
+
#
|
27
|
+
# xtractr.flows('flow.service:HTTP flow.duration:[1 300]').count('flow.src')
|
28
|
+
class Count
|
29
|
+
attr_reader :xtractr # :nodoc:
|
30
|
+
|
31
|
+
# Returns the field used for counting.
|
32
|
+
attr_reader :field
|
33
|
+
|
34
|
+
# Returns the unique value of the field.
|
35
|
+
attr_reader :value
|
36
|
+
|
37
|
+
# Returns the count of the field/value.
|
38
|
+
attr_reader :count
|
39
|
+
|
40
|
+
def initialize xtractr, field, value, count # :nodoc:
|
41
|
+
@xtractr = xtractr
|
42
|
+
@field = field
|
43
|
+
@value = value
|
44
|
+
@count = count
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a Field::Value object that can be used for further method
|
48
|
+
# chaining.
|
49
|
+
# xtractr.flows.count('flow.src').first.object.count('flow.service')
|
50
|
+
def object
|
51
|
+
Field::Value.new xtractr, "key" => field.name, "value" => value
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fetch the list of packets that contain this field value.
|
55
|
+
# xtractr.flows.count('flow.src').first.packets.each { |pkt ... }
|
56
|
+
def packets q=nil
|
57
|
+
object.packets q
|
58
|
+
end
|
59
|
+
|
60
|
+
# Iterate over each packet that contains this field value.
|
61
|
+
# xtractr.flows.count('flow.src').first.each_packet { |pkt ... }
|
62
|
+
def each_packet(q=nil, &blk) # :yields: packet
|
63
|
+
packets(q).each(&blk)
|
64
|
+
return self
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sum the numeric values of vfield, keyed by the unique values of
|
68
|
+
# kfield. This is used for method chaining.
|
69
|
+
# xtractr.flows.count('flow.src').first.sum('flow.service', 'flow.bytes')
|
70
|
+
def sum kfield, vfield
|
71
|
+
object.sum kfield, vfield
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect # :nodoc:
|
75
|
+
"#<count #{value} #{count}>"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# = Sum
|
80
|
+
# Sum contains the results of doing a map/reduce on either flows or
|
81
|
+
# packets. Each sum contains the field on which the map/reduce was
|
82
|
+
# performed, the unique value as all as the sum of that value in the
|
83
|
+
# flows or packets. For example to count the bytes sent in HTTP flows
|
84
|
+
# keyed by the source IP address, you would do:
|
85
|
+
#
|
86
|
+
# xtractr.flows('flow.service:HTTP').count('flow.src', 'flow.bytes')
|
87
|
+
class Sum
|
88
|
+
attr_reader :xtractr # :nodoc:
|
89
|
+
|
90
|
+
# Returns the field used for summing.
|
91
|
+
attr_reader :field
|
92
|
+
|
93
|
+
# Returns the unique value used as the map/reduce key.
|
94
|
+
attr_reader :value
|
95
|
+
|
96
|
+
# Returns the aggregate computed sum.
|
97
|
+
attr_reader :sum
|
98
|
+
|
99
|
+
def initialize xtractr, field, value, sum # :nodoc:
|
100
|
+
@xtractr = xtractr
|
101
|
+
@field = field
|
102
|
+
@value = value
|
103
|
+
@sum = sum
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns a Field::Value object that can be used for further method
|
107
|
+
# chaining. In the following example, we first compute the top talkers
|
108
|
+
# (based on the bytes sent) and then use the topmost talker to count
|
109
|
+
# the list of unique services.
|
110
|
+
# xtractr.flows.sum('flow.src', 'flow.bytes').first.object.count('flow.service')
|
111
|
+
def object
|
112
|
+
Field::Value.new xtractr, "key" => field.name, "value" => value
|
113
|
+
end
|
114
|
+
|
115
|
+
# Fetch the list of packets that contain this field value.
|
116
|
+
# xtractr.flows.sum('flow.src', 'flow.bytes').first.packets.each { |pkt ... }
|
117
|
+
def packets q=nil
|
118
|
+
object.packets q
|
119
|
+
end
|
120
|
+
|
121
|
+
# Iterate over each packet that contains this field value.
|
122
|
+
# xtractr.flows.sum('flow.src', 'flow.bytes').first.each_packet { |pkt ... }
|
123
|
+
def each_packet q=nil, &blk
|
124
|
+
packets(q).each(&blk)
|
125
|
+
return self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Count the unique values of the specified field amongst all the packets
|
129
|
+
# that matched the query.
|
130
|
+
# xtractr.flows.sum('flow.src', 'flow.bytes').first.count('flow.service')
|
131
|
+
def count _field
|
132
|
+
object.count _field
|
133
|
+
end
|
134
|
+
|
135
|
+
def inspect # :nodoc:
|
136
|
+
"#<sum #{value} #{sum}>"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.count xtractr, field, url, opts={} # :nodoc:
|
141
|
+
field = Field.new(xtractr, field) if field.is_a? String
|
142
|
+
name = field.name.gsub /^(pkt|flow)\./, ''
|
143
|
+
_opts = opts.dup
|
144
|
+
_opts[:r] = <<-EOS
|
145
|
+
({
|
146
|
+
map: function(_pf) {
|
147
|
+
_pf.values("#{name}", function(_value) {
|
148
|
+
if (_value) {
|
149
|
+
if (typeof(_value) === 'string') {
|
150
|
+
if (_value.length > 1024) {
|
151
|
+
_value = _value.slice(0,1024);
|
152
|
+
}
|
153
|
+
}
|
154
|
+
emit(_value, 1);
|
155
|
+
}
|
156
|
+
});
|
157
|
+
},
|
158
|
+
reduce: function(_key, _values) {
|
159
|
+
return sum(_values);
|
160
|
+
}
|
161
|
+
})
|
162
|
+
EOS
|
163
|
+
result = xtractr.json url, _opts
|
164
|
+
result['rows'].map do |row|
|
165
|
+
Views::Count.new(xtractr, field, row['key'], row['value'])
|
166
|
+
end.sort { |a, b| b.count <=> a.count }
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.sum xtractr, kfield, vfield, url, opts={} # :nodoc:
|
170
|
+
kfield = Field.new(xtractr, kfield) if kfield.is_a? String
|
171
|
+
vfield = Field.new(xtractr, vfield) if vfield.is_a? String
|
172
|
+
kname = kfield.name.gsub /^(pkt|flow)\./, ''
|
173
|
+
vname = vfield.name.gsub /^(pkt|flow)\./, ''
|
174
|
+
_opts = opts.dup
|
175
|
+
_opts[:r] = <<-EOS
|
176
|
+
({
|
177
|
+
map: function(_pf) {
|
178
|
+
var _key = _pf["#{kname}"];
|
179
|
+
if (_key) {
|
180
|
+
if (typeof(_key) === 'string') {
|
181
|
+
if (_key.length > 1024) {
|
182
|
+
_key = _key.slice(0,1024);
|
183
|
+
}
|
184
|
+
}
|
185
|
+
_pf.values("#{vname}", function(_val) {
|
186
|
+
if (typeof(_val) === 'number') {
|
187
|
+
emit(_key, _val);
|
188
|
+
}
|
189
|
+
});
|
190
|
+
}
|
191
|
+
},
|
192
|
+
reduce: function(_key, _values) {
|
193
|
+
return sum(_values);
|
194
|
+
}
|
195
|
+
})
|
196
|
+
EOS
|
197
|
+
result = xtractr.json url, _opts
|
198
|
+
result['rows'].map do |row|
|
199
|
+
Views::Sum.new(xtractr, kfield, row['key'], row['value'])
|
200
|
+
end.sort { |a, b| b.sum <=> a.sum }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end # Xtractr
|
204
|
+
end # Mu
|