ruby-druid 0.1.1

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