ruby-druid 0.1.1

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,66 @@
1
+ require 'active_support/time'
2
+ require 'ap'
3
+ require 'forwardable'
4
+ require 'irb'
5
+ require 'ripl'
6
+ require 'terminal-table'
7
+
8
+ require 'druid'
9
+
10
+ Ripl::Shell.class_eval do
11
+ def format_query_result(result, query)
12
+
13
+ include_timestamp = query.properties[:granularity] != 'all'
14
+
15
+ keys = result.empty? ? [] : result.last.keys
16
+
17
+ Terminal::Table.new({
18
+ headings: (include_timestamp ? ["timestamp"] : []) + keys,
19
+ rows: result.map { |row| (include_timestamp ? [row.timestamp] : []) + row.values }
20
+ })
21
+ end
22
+
23
+ def format_result(result)
24
+ if result.is_a?(Druid::Query)
25
+ puts format_query_result(result.send, result)
26
+ else
27
+ ap(result)
28
+ end
29
+ end
30
+ end
31
+
32
+ module Druid
33
+ class Console
34
+
35
+ extend Forwardable
36
+
37
+ def initialize(uri, source, options)
38
+ @uri, @source, @options = uri, source, options
39
+ Ripl.start(binding: binding)
40
+ end
41
+
42
+ def client
43
+ @client ||= Druid::Client.new(@uri, @options)
44
+ @source ||= @client.data_sources[0]
45
+ @client
46
+ end
47
+
48
+ def source
49
+ client.data_source(@source)
50
+ end
51
+
52
+ def dimensions
53
+ source.dimensions
54
+ end
55
+
56
+ def metrics
57
+ source.metrics
58
+ end
59
+
60
+ def query
61
+ client.query(@source)
62
+ end
63
+
64
+ def_delegators :query, :group_by, :sum, :long_sum, :postagg, :interval, :granularity, :filter, :time_series
65
+ end
66
+ end
@@ -0,0 +1,216 @@
1
+ module Druid
2
+ class Filter
3
+ (instance_methods + private_instance_methods).each do |method|
4
+ unless method.to_s =~ /^(__|instance_eval|instance_exec|initialize|object_id|raise|puts|inspect)/ || method.to_s =~ /\?/
5
+ undef_method method
6
+ end
7
+ end
8
+
9
+ def method_missing(method_id, *args)
10
+ FilterDimension.new(method_id)
11
+ end
12
+ end
13
+
14
+ class FilterParameter
15
+ (instance_methods + private_instance_methods).each do |method|
16
+ unless method.to_s =~ /^(__|instance_eval|instance_exec|initialize|object_id|raise|puts|inspect|class)/ || method.to_s =~ /\?/
17
+ undef_method method
18
+ end
19
+ end
20
+
21
+ def to_s
22
+ to_hash.to_s
23
+ end
24
+
25
+ def as_json(*a)
26
+ to_hash
27
+ end
28
+
29
+ def to_json(*a)
30
+ to_hash.to_json(*a)
31
+ end
32
+ end
33
+
34
+ class FilterDimension < FilterParameter
35
+ def initialize(name)
36
+ @name = name
37
+ @value = nil
38
+ @regexp = nil
39
+ end
40
+
41
+ def eq(value)
42
+ return self.in(value) if value.is_a? Array
43
+ return self.regexp(value) if value.is_a? Regexp
44
+ @value = value
45
+ self
46
+ end
47
+
48
+ alias :'==' :eq
49
+
50
+
51
+ def neq(value)
52
+ return !self.in(value)
53
+ end
54
+
55
+ alias :'!=' :neq
56
+
57
+ def in(*args)
58
+ values = args.flatten
59
+ raise "Must provide non-empty array in in()" if values.empty?
60
+
61
+ if (values.length == 1)
62
+ return self.eq(values[0])
63
+ end
64
+
65
+ filter_or = FilterOperator.new('or', true)
66
+ values.each do |value|
67
+ raise "query is too complex" if value.is_a? FilterParameter
68
+ param = FilterDimension.new(@name)
69
+ param.eq value
70
+ filter_or.add param
71
+ end
72
+ filter_or
73
+ end
74
+
75
+ def &(other)
76
+ filter_and = FilterOperator.new('and', true)
77
+ filter_and.add(self)
78
+ filter_and.add(other)
79
+ filter_and
80
+ end
81
+
82
+ def |(other)
83
+ filter_or = FilterOperator.new('or', true)
84
+ filter_or.add(self)
85
+ filter_or.add(other)
86
+ filter_or
87
+ end
88
+
89
+ def !()
90
+ filter_not = FilterOperator.new('not', false)
91
+ filter_not.add(self)
92
+ filter_not
93
+ end
94
+
95
+ def >(value)
96
+ filter_js = FilterJavascript.new_comparison(@name, '>', value)
97
+ filter_js
98
+ end
99
+
100
+ def <(value)
101
+ filter_js = FilterJavascript.new_comparison(@name, '<', value)
102
+ filter_js
103
+ end
104
+
105
+ def >=(value)
106
+ filter_js = FilterJavascript.new_comparison(@name, '>=', value)
107
+ filter_js
108
+ end
109
+
110
+ def <=(value)
111
+ filter_js = FilterJavascript.new_comparison(@name, '<=', value)
112
+ filter_js
113
+ end
114
+
115
+ def javascript(js)
116
+ filter_js = FilterJavascript.new(@name, js)
117
+ filter_js
118
+ end
119
+
120
+ def regexp(r)
121
+ r = Regexp.new(r) unless r.is_a? Regexp
122
+ @regexp = r.inspect[1...-1] #to_s doesn't work
123
+ self
124
+ end
125
+
126
+ def to_hash
127
+ raise 'no value assigned' unless @value.nil? ^ @regexp.nil?
128
+ hash = {
129
+ :dimension => @name
130
+ }
131
+ if @value
132
+ hash['type'] = 'selector'
133
+ hash['value'] = @value
134
+ elsif @regexp
135
+ hash['type'] = 'regex'
136
+ hash['pattern'] = @regexp
137
+ end
138
+ hash
139
+ end
140
+ end
141
+
142
+ class FilterOperator < FilterParameter
143
+ def initialize(name, takes_many)
144
+ @name = name
145
+ @takes_many = takes_many
146
+ @elements = []
147
+ end
148
+
149
+ def add(element)
150
+ @elements.push element
151
+ end
152
+
153
+ def &(other)
154
+ if @name == 'and'
155
+ filter_and = self
156
+ else
157
+ filter_and = FilterOperator.new('and', true)
158
+ filter_and.add(self)
159
+ end
160
+ filter_and.add(other)
161
+ filter_and
162
+ end
163
+
164
+ def |(other)
165
+ if @name == 'or'
166
+ filter_or = self
167
+ else
168
+ filter_or = FilterOperator.new('or', true)
169
+ filter_or.add(self)
170
+ end
171
+ filter_or.add(other)
172
+ filter_or
173
+ end
174
+
175
+ def !()
176
+ if @name == 'not'
177
+ @elements[0]
178
+ else
179
+ filter_not = FilterOperator.new('not', false)
180
+ filter_not.add(self)
181
+ filter_not
182
+ end
183
+ end
184
+
185
+ def to_hash
186
+ result = {
187
+ :type => @name
188
+ }
189
+ if @takes_many
190
+ result[:fields] = @elements.map(&:to_hash)
191
+ else
192
+ result[:field] = @elements[0].to_hash
193
+ end
194
+ result
195
+ end
196
+ end
197
+
198
+ class FilterJavascript < FilterDimension
199
+ def initialize(dimension, expression)
200
+ @dimension = dimension
201
+ @expression = expression
202
+ end
203
+
204
+ def self.new_comparison(dimension, operator, value)
205
+ self.new(dimension, "#{dimension} #{operator} #{value.is_a?(String) ? "'#{value}'" : value}")
206
+ end
207
+
208
+ def to_hash
209
+ {
210
+ :type => 'javascript',
211
+ :dimension => @dimension,
212
+ :function => "function(#{@dimension}) { return(#{@expression}); }"
213
+ }
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,53 @@
1
+ module Druid
2
+ class Having
3
+ def method_missing(name, *args)
4
+ if args.empty?
5
+ HavingClause.new(name)
6
+ end
7
+ end
8
+ end
9
+
10
+ class HavingClause
11
+ (instance_methods + private_instance_methods).each do |method|
12
+ unless method.to_s =~ /^(__|instance_eval|instance_exec|initialize|object_id|raise|puts|inspect|class)/ || method.to_s =~ /\?/
13
+ undef_method method
14
+ end
15
+ end
16
+
17
+ def initialize(metric)
18
+ @metric = metric
19
+ end
20
+
21
+ def <(value)
22
+ @type = "lessThan"
23
+ @value = value
24
+ self
25
+ end
26
+
27
+ def >(value)
28
+ @type = "greaterThan"
29
+ @value = value
30
+ self
31
+ end
32
+
33
+ def to_s
34
+ to_hash.to_s
35
+ end
36
+
37
+ def as_json(*a)
38
+ to_hash
39
+ end
40
+
41
+ def to_json(*a)
42
+ to_hash.to_json(*a)
43
+ end
44
+
45
+ def to_hash
46
+ {
47
+ :type => @type,
48
+ :aggregation => @metric,
49
+ :value => @value
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,111 @@
1
+ module Druid
2
+ class PostAggregation
3
+ def method_missing(name, *args)
4
+ if args.empty?
5
+ PostAggregationField.new(name)
6
+ end
7
+ end
8
+ end
9
+
10
+ module PostAggregationOperators
11
+ def +(value)
12
+ PostAggregationOperation.new(self, :+, value)
13
+ end
14
+
15
+ def -(value)
16
+ PostAggregationOperation.new(self, :-, value)
17
+ end
18
+
19
+ def *(value)
20
+ PostAggregationOperation.new(self, :*, value)
21
+ end
22
+
23
+ def /(value)
24
+ PostAggregationOperation.new(self, :/, value)
25
+ end
26
+ end
27
+
28
+ class PostAggregationOperation
29
+ include PostAggregationOperators
30
+
31
+ attr_reader :left, :operator, :right, :name
32
+
33
+ def initialize(left, operator, right)
34
+ @left = left.is_a?(Numeric) ? PostAggregationConstant.new(left) : left
35
+ @operator = operator
36
+ @right = right.is_a?(Numeric) ? PostAggregationConstant.new(right) : right
37
+ end
38
+
39
+ def as(field)
40
+ @name = field.name.to_s
41
+ self
42
+ end
43
+
44
+ def get_field_names
45
+ field_names = []
46
+ field_names << left.get_field_names if left.respond_to?(:get_field_names)
47
+ field_names << right.get_field_names if right.respond_to?(:get_field_names)
48
+ field_names
49
+ end
50
+
51
+ def to_hash
52
+ hash = { "type" => "arithmetic", "fn" => @operator, "fields" => [@left.to_hash, @right.to_hash] }
53
+ hash["name"] = @name if @name
54
+ hash
55
+ end
56
+
57
+ def to_json(*a)
58
+ to_hash.to_json(*a)
59
+ end
60
+
61
+ def as_json(*a)
62
+ to_hash
63
+ end
64
+ end
65
+
66
+ class PostAggregationField
67
+ include PostAggregationOperators
68
+
69
+ attr_reader :name
70
+
71
+ def initialize(name)
72
+ @name = name
73
+ end
74
+
75
+ def get_field_names
76
+ @name
77
+ end
78
+
79
+ def to_hash
80
+ { "type" => "fieldAccess", "name" => @name, "fieldName" => @name }
81
+ end
82
+
83
+ def to_json(*a)
84
+ to_hash.to_json(*a)
85
+ end
86
+
87
+ def as_json(*a)
88
+ to_hash
89
+ end
90
+ end
91
+
92
+ class PostAggregationConstant
93
+ attr_reader :value
94
+
95
+ def initialize(value)
96
+ @value = value
97
+ end
98
+
99
+ def to_hash
100
+ { "type" => "constant", "value" => @value }
101
+ end
102
+
103
+ def to_json(*a)
104
+ to_hash.to_json(*a)
105
+ end
106
+
107
+ def as_json(*a)
108
+ to_hash
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,175 @@
1
+ require 'druid/filter'
2
+ require 'druid/having'
3
+ require 'druid/post_aggregation'
4
+ require 'time'
5
+ require 'json'
6
+
7
+ module Druid
8
+ class Query
9
+
10
+ attr_reader :properties
11
+
12
+ def initialize(source, client = nil)
13
+ @properties = {}
14
+ @client = client
15
+
16
+ # set some defaults
17
+ data_source(source)
18
+ granularity(:all)
19
+
20
+ interval(today)
21
+ end
22
+
23
+ def today
24
+ Time.now.to_date.to_time
25
+ end
26
+
27
+ def send
28
+ @client.send(self)
29
+ end
30
+
31
+ def query_type(type)
32
+ @properties[:queryType] = type
33
+ self
34
+ end
35
+
36
+ def get_query_type()
37
+ @properties[:queryType] || :groupBy
38
+ end
39
+
40
+ def data_source(source)
41
+ source = source.split('/')
42
+ @properties[:dataSource] = source.last
43
+ @service = source.first
44
+ self
45
+ end
46
+
47
+ def source
48
+ "#{@service}/#{@properties[:dataSource]}"
49
+ end
50
+
51
+ def group_by(*dimensions)
52
+ query_type(:groupBy)
53
+ @properties[:dimensions] = dimensions.flatten
54
+ self
55
+ end
56
+
57
+ def time_series(*aggregations)
58
+ query_type(:timeseries)
59
+ #@properties[:aggregations] = aggregations.flatten
60
+ self
61
+ end
62
+
63
+ [:long_sum, :double_sum].each do |method_name|
64
+ agg_type = method_name.to_s.split('_')
65
+ agg_type[1].capitalize!
66
+ agg_type = agg_type.join
67
+
68
+ define_method method_name do |*metrics|
69
+ query_type(get_query_type())
70
+ aggregations = @properties[:aggregations] || []
71
+ aggregations.concat(metrics.flatten.map{ |metric|
72
+ {
73
+ :type => agg_type,
74
+ :name => metric.to_s,
75
+ :fieldName => metric.to_s
76
+ }
77
+ }).uniq!
78
+ @properties[:aggregations] = aggregations
79
+ self
80
+ end
81
+ end
82
+
83
+ alias_method :sum, :long_sum
84
+
85
+ def postagg(type=:long, &block)
86
+ post_agg = PostAggregation.new.instance_exec(&block)
87
+ @properties[:postAggregations] ||= []
88
+ @properties[:postAggregations] << post_agg
89
+
90
+ # make sure, the required fields are in the query
91
+ field_type = (type.to_s + '_sum').to_sym
92
+ # ugly workaround, because SOMEONE overwrote send
93
+ sum_method = self.method(field_type)
94
+ sum_method.call(post_agg.get_field_names)
95
+
96
+ self
97
+ end
98
+
99
+ def postagg_double(&block)
100
+ postagg(:double, &block)
101
+ end
102
+
103
+ def filter(hash = nil, &block)
104
+ if hash
105
+ last = nil
106
+ hash.each do |k,values|
107
+ filter = FilterDimension.new(k).in(values)
108
+ last = last ? last.&(filter) : filter
109
+ end
110
+ @properties[:filter] = @properties[:filter] ? @properties[:filter].&(last) : last
111
+ end
112
+ if block
113
+ filter = Filter.new.instance_exec(&block)
114
+ raise "Not a valid filter" unless filter.is_a? FilterParameter
115
+ @properties[:filter] = @properties[:filter] ? @properties[:filter].&(filter) : filter
116
+ end
117
+ self
118
+ end
119
+
120
+ def interval(from, to = Time.now)
121
+ intervals([[from, to]])
122
+ end
123
+
124
+ def intervals(is)
125
+ @properties[:intervals] = is.map{ |ii| mk_interval(ii[0], ii[1]) }
126
+ self
127
+ end
128
+
129
+ def having(&block)
130
+ having = Having.new.instance_exec(&block)
131
+ @properties[:having] = having
132
+ self
133
+ end
134
+
135
+ alias_method :[], :interval
136
+
137
+ def granularity(gran, time_zone = nil)
138
+ gran = gran.to_s
139
+ case gran
140
+ when 'none', 'all', 'second', 'minute', 'fifteen_minute', 'thirty_minute', 'hour'
141
+ @properties[:granularity] = gran
142
+ return self
143
+ when 'day'
144
+ gran = 'P1D'
145
+ end
146
+
147
+ time_zone ||= Time.now.strftime('%Z')
148
+ # druid doesn't seem to understand 'CEST'
149
+ # this is a work around
150
+ time_zone = 'Europe/Berlin' if time_zone == 'CEST'
151
+
152
+ @properties[:granularity] = {
153
+ :type => 'period',
154
+ :period => gran,
155
+ :timeZone => time_zone
156
+ }
157
+ self
158
+ end
159
+
160
+ def to_json
161
+ @properties.to_json
162
+ end
163
+
164
+ private
165
+
166
+ def mk_interval(from, to)
167
+ from = today + from if from.is_a?(Fixnum)
168
+ to = today + to if to.is_a?(Fixnum)
169
+
170
+ from = DateTime.parse(from.to_s) unless from.respond_to? :iso8601
171
+ to = DateTime.parse(to.to_s) unless to.respond_to? :iso8601
172
+ "#{from.iso8601}/#{to.iso8601}"
173
+ end
174
+ end
175
+ end