ruby-druid 0.9.0 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 804e71f527f98f4bc082a2967da5ce7065fa2c01
4
- data.tar.gz: ada587e143c21e913df7386ebb3020155d82ff5d
2
+ SHA256:
3
+ metadata.gz: 5c242b82f9ca73bc79a18d0fed69b6f956f515900c363bd7878b53f10eaad62d
4
+ data.tar.gz: e1699daefede54f60a9ac0e2be9b175b8d6dfd356c4550a36421933bd3e57866
5
5
  SHA512:
6
- metadata.gz: be6eb7e1e566340d22c70b7bc693e3849260b9994886249b501d90db10fcbfac1b0074928e7e5118cc704b5b8eaeda5c02e8e24b3a6b05b0a03e31137090929e
7
- data.tar.gz: 08d4a836a5ebc98cbd8c923a1ab718b9c67cceaa66be5e2ab218e85c2928564f47ed05ad2648a977b079f1691c96c35e273bf70494a5c50d9944636218e668fd
6
+ metadata.gz: c9e438973e1a8816ebbd293a409aafbad9c015a29ffa93a55826bf4c13c741f223eb30597e059f7b35357941f818cdba791a9337ea0cb0833c45c6fd7a9a5842
7
+ data.tar.gz: 5ac8395f4155f4c830bf6518e292a4fc7a7ce9b54df8337931072e5cd49409c1a54d739182ce82f6a148fcd040c8c3c0dde772d06ff192a0605363d27408cdfa
data/README.md CHANGED
@@ -29,32 +29,22 @@ gem install ruby-druid
29
29
 
30
30
  ## Usage
31
31
 
32
- ```ruby
33
- Druid::Client.new('zk1:2181,zk2:2181/druid').query('service/source')
34
- ```
35
-
36
- returns a query object on which all other methods can be called to create a full and valid Druid query.
37
-
38
- A query object can be sent like this:
32
+ A query can be constructed and sent like so:
39
33
 
40
34
  ```ruby
41
- client = Druid::Client.new('zk1:2181,zk2:2181/druid')
42
- query = Druid::Query.new('service/source')
43
- client.send(query)
35
+ data_source = Druid::Client.new('zk1:2181,zk2:2181/druid').data_source('service/source')
36
+ query = Druid::Query::Builder.new.long_sum(:aggregate1).last(1.day).granularity(:all)
37
+ result = data_source.post(query)
44
38
  ```
45
39
 
46
- The `send` method returns the parsed response from the druid server as an array. If the response is not empty it contains one `ResponseRow` object for each row. The timestamp by can be received by a method with the same name (i.e. `row.timestamp`), all row values by hashlike syntax (i.e. `row['dimension'])
40
+ The `post` method on the `DataSource` returns the parsed response from the Druid server as an array.
47
41
 
48
- An options hash can be passed when creating `Druid::Client` instance:
42
+ If you don't want to use ZooKeeper for broker discovery, you can explicitly construct a `DataSource`:
49
43
 
50
44
  ```ruby
51
- client = Druid::Client.new('zk1:2181,zk2:2181/druid', http_timeout: 20)
45
+ data_source = Druid::DataSource.new('service/source', 'http://localhost:8080/druid/v2')
52
46
  ```
53
47
 
54
- Supported options are:
55
- * `static_setup` to explicitly specify a broker url, e.g. `static_setup: { 'my/source_name' => 'http://1.2.3.4:8080/druid/v2/' }`
56
- * `http_timeout` to define a timeout for sending http queries to a broker (in minutes, default value is 2)
57
-
58
48
  ### GroupBy
59
49
 
60
50
  A [GroupByQuery](http://druid.io/docs/latest/querying/groupbyquery.html) sets the
@@ -63,7 +53,7 @@ dimensions to group the data.
63
53
  `queryType` is set automatically to `groupBy`.
64
54
 
65
55
  ```ruby
66
- Druid::Query.new('service/source').group_by([:dimension1, :dimension2])
56
+ Druid::Query::Builder.new.group_by([:dimension1, :dimension2])
67
57
  ```
68
58
 
69
59
  ### TimeSeries
@@ -71,7 +61,7 @@ Druid::Query.new('service/source').group_by([:dimension1, :dimension2])
71
61
  A [TimeSeriesQuery](http://druid.io/docs/latest/querying/timeseriesquery.html) returns an array of JSON objects where each object represents a value asked for by the timeseries query.
72
62
 
73
63
  ```ruby
74
- Druid::Query.new('service/source').time_series([:aggregate1, :aggregate2])
64
+ Druid::Query::Builder.new.time_series([:aggregate1, :aggregate2])
75
65
  ```
76
66
 
77
67
  ### Aggregations
@@ -79,7 +69,7 @@ Druid::Query.new('service/source').time_series([:aggregate1, :aggregate2])
79
69
  #### longSum, doubleSum, count, min, max, hyperUnique
80
70
 
81
71
  ```ruby
82
- Druid::Query.new('service/source').long_sum([:aggregate1, :aggregate2])
72
+ Druid::Query::Builder.new.long_sum([:aggregate1, :aggregate2])
83
73
  ```
84
74
 
85
75
  In the same way could be used the following methods for [aggregations](http://druid.io/docs/latest/querying/aggregations.html) adding: `double_sum, count, min, max, hyper_unique`
@@ -87,7 +77,7 @@ In the same way could be used the following methods for [aggregations](http://dr
87
77
  #### cardinality
88
78
 
89
79
  ```ruby
90
- Druid::Query.new('service/source').cardinality(:aggregate, [:dimension1, dimension2], <by_row: true | false>)
80
+ Druid::Query::Builder.new.cardinality(:aggregate, [:dimension1, dimension2], <by_row: true | false>)
91
81
  ```
92
82
 
93
83
  #### javascript
@@ -95,19 +85,29 @@ Druid::Query.new('service/source').cardinality(:aggregate, [:dimension1, dimensi
95
85
  For example calculation for `sum(log(x)/y) + 10`:
96
86
 
97
87
  ```ruby
98
- Druid::Query.new('service/source').js_aggregation(:aggregate, [:x, :y],
88
+ Druid::Query::Builder.new.js_aggregation(:aggregate, [:x, :y],
99
89
  aggregate: "function(current, a, b) { return current + (Math.log(a) * b); }",
100
90
  combine: "function(partialA, partialB) { return partialA + partialB; }",
101
91
  reset: "function() { return 10; }"
102
92
  )
103
93
  ```
104
94
 
95
+ #### filtered aggregation
96
+
97
+ A filtered aggregator wraps any given aggregator, but only aggregates the values for which the given dimension filter matches.
98
+
99
+ ```ruby
100
+ Druid::Query::Builder.new.filtered_aggregation(:aggregate1, :aggregate_1_name, :longSum) do
101
+ dimension1.neq 1 & dimension2.neq 2
102
+ end
103
+ ```
104
+
105
105
  ### Post Aggregations
106
106
 
107
107
  A simple syntax for post aggregations with +,-,/,* can be used like:
108
108
 
109
109
  ```ruby
110
- query = Druid::Query.new('service/source').long_sum([:aggregate1, :aggregate2])
110
+ query = Druid::Query::Builder.new.long_sum([:aggregate1, :aggregate2])
111
111
  query.postagg { (aggregate2 + aggregate2).as output_field_name }
112
112
  ```
113
113
 
@@ -124,7 +124,7 @@ query.postagg { js('function(aggregate1, aggregate2) { return aggregate1 + aggre
124
124
  The interval for the query takes a string with date and time or objects that provide an `iso8601` method.
125
125
 
126
126
  ```ruby
127
- query = Druid::Query.new('service/source').long_sum(:aggregate1)
127
+ query = Druid::Query::Builder.new.long_sum(:aggregate1)
128
128
  query.interval("2013-01-01T00", Time.now)
129
129
  ```
130
130
 
@@ -139,14 +139,14 @@ The period `'day'` or `:day` will be interpreted as `'P1D'`.
139
139
  If a period granularity is specifed, the (optional) second parameter is a time zone. It defaults to the machines local time zone. i.e.
140
140
 
141
141
  ```ruby
142
- query = Druid::Query.new('service/source').long_sum(:aggregate1)
142
+ query = Druid::Query::Builder.new.long_sum(:aggregate1)
143
143
  query.granularity(:day)
144
144
  ```
145
145
 
146
146
  is (on my box) the same as
147
147
 
148
148
  ```ruby
149
- query = Druid::Query.new('service/source').long_sum(:aggregate1)
149
+ query = Druid::Query::Builder.new.long_sum(:aggregate1)
150
150
  query.granularity('P1D', 'Europe/Berlin')
151
151
  ```
152
152
 
@@ -154,18 +154,18 @@ query.granularity('P1D', 'Europe/Berlin')
154
154
 
155
155
  ```ruby
156
156
  # equality
157
- Druid::Query.new('service/source').having { metric == 10 }
157
+ Druid::Query::Builder.new.having { metric == 10 }
158
158
  ```
159
159
 
160
160
  ```ruby
161
161
  # inequality
162
- Druid::Query.new('service/source').having { metric != 10 }
162
+ Druid::Query::Builder.new.having { metric != 10 }
163
163
  ```
164
164
 
165
165
  ```ruby
166
166
  # greater, less
167
- Druid::Query.new('service/source').having { metric > 10 }
168
- Druid::Query.new('service/source').having { metric < 10 }
167
+ Druid::Query::Builder.new.having { metric > 10 }
168
+ Druid::Query::Builder.new.having { metric < 10 }
169
169
  ```
170
170
 
171
171
  #### Compound having filters
@@ -174,17 +174,17 @@ Having filters can be combined with boolean logic.
174
174
 
175
175
  ```ruby
176
176
  # and
177
- Druid::Query.new('service/source').having { (metric != 1) & (metric2 != 2) }
177
+ Druid::Query::Builder.new.having { (metric != 1) & (metric2 != 2) }
178
178
  ```
179
179
 
180
180
  ```ruby
181
181
  # or
182
- Druid::Query.new('service/source').having { (metric == 1) | (metric2 == 2) }
182
+ Druid::Query::Builder.new.having { (metric == 1) | (metric2 == 2) }
183
183
  ```
184
184
 
185
185
  ```ruby
186
186
  # not
187
- Druid::Query.new('service/source').having{ !metric.eq(1) }
187
+ Druid::Query::Builder.new.having{ !metric.eq(1) }
188
188
  ```
189
189
 
190
190
  ### Filters
@@ -197,27 +197,27 @@ Filters can be chained `filter{...}.filter{...}`
197
197
 
198
198
  ```ruby
199
199
  # equality
200
- Druid::Query.new('service/source').filter{dimension.eq 1}
201
- Druid::Query.new('service/source').filter{dimension == 1}
200
+ Druid::Query::Builder.new.filter{dimension.eq 1}
201
+ Druid::Query::Builder.new.filter{dimension == 1}
202
202
  ```
203
203
 
204
204
  ```ruby
205
205
  # inequality
206
- Druid::Query.new('service/source').filter{dimension.neq 1}
207
- Druid::Query.new('service/source').filter{dimension != 1}
206
+ Druid::Query::Builder.new.filter{dimension.neq 1}
207
+ Druid::Query::Builder.new.filter{dimension != 1}
208
208
  ```
209
209
 
210
210
  ```ruby
211
211
  # greater, less
212
- Druid::Query.new('service/source').filter{dimension > 1}
213
- Druid::Query.new('service/source').filter{dimension >= 1}
214
- Druid::Query.new('service/source').filter{dimension < 1}
215
- Druid::Query.new('service/source').filter{dimension <= 1}
212
+ Druid::Query::Builder.new.filter{dimension > 1}
213
+ Druid::Query::Builder.new.filter{dimension >= 1}
214
+ Druid::Query::Builder.new.filter{dimension < 1}
215
+ Druid::Query::Builder.new.filter{dimension <= 1}
216
216
  ```
217
217
 
218
218
  ```ruby
219
219
  # JavaScript
220
- Druid::Query.new('service/source').filter{a.javascript('dimension >= 1 && dimension < 5')}
220
+ Druid::Query::Builder.new.filter{a.javascript('dimension >= 1 && dimension < 5')}
221
221
  ```
222
222
 
223
223
  #### Compound Filters
@@ -226,17 +226,17 @@ Filters can be combined with boolean logic.
226
226
 
227
227
  ```ruby
228
228
  # and
229
- Druid::Query.new('service/source').filter{dimension.neq 1 & dimension2.neq 2}
229
+ Druid::Query::Builder.new.filter{dimension.neq 1 & dimension2.neq 2}
230
230
  ```
231
231
 
232
232
  ```ruby
233
233
  # or
234
- Druid::Query.new('service/source').filter{dimension.neq 1 | dimension2.neq 2}
234
+ Druid::Query::Builder.new.filter{dimension.neq 1 | dimension2.neq 2}
235
235
  ```
236
236
 
237
237
  ```ruby
238
238
  # not
239
- Druid::Query.new('service/source').filter{!dimension.eq(1)}
239
+ Druid::Query::Builder.new.filter{!dimension.eq(1)}
240
240
  ```
241
241
 
242
242
  #### Inclusion Filter
@@ -244,18 +244,18 @@ Druid::Query.new('service/source').filter{!dimension.eq(1)}
244
244
  This filter creates a set of equals filters in an or filter.
245
245
 
246
246
  ```ruby
247
- Druid::Query.new('service/source').filter{dimension.in(1,2,3)}
247
+ Druid::Query::Builder.new.filter{dimension.in(1,2,3)}
248
248
  ```
249
249
  #### Geographic filter
250
250
 
251
251
  These filters have to be combined with time_series and do only work when coordinates is a spatial dimension [GeographicQueries](http://druid.io/docs/latest/development/geo.html)
252
252
 
253
253
  ```ruby
254
- Druid::Query.new('service/source').time_series().long_sum([:aggregate1]).filter{coordinates.in_rec [[50.0,13.0],[54.0,15.0]]}
254
+ Druid::Query::Builder.new.time_series().long_sum([:aggregate1]).filter{coordinates.in_rec [[50.0,13.0],[54.0,15.0]]}
255
255
  ```
256
256
 
257
257
  ```ruby
258
- Druid::Query.new('service/source').time_series().long_sum([:aggregate1]).filter{coordinates.in_circ [[53.0,13.0], 5.0]}
258
+ Druid::Query::Builder.new.time_series().long_sum([:aggregate1]).filter{coordinates.in_circ [[53.0,13.0], 5.0]}
259
259
  ```
260
260
 
261
261
  #### Exclusion Filter
@@ -263,7 +263,7 @@ Druid::Query.new('service/source').time_series().long_sum([:aggregate1]).filter{
263
263
  This filter creates a set of not-equals fitlers in an and filter.
264
264
 
265
265
  ```ruby
266
- Druid::Query.new('service/source').filter{dimension.nin(1,2,3)}
266
+ Druid::Query::Builder.new.filter{dimension.nin(1,2,3)}
267
267
  ```
268
268
 
269
269
  #### Hash syntax
@@ -271,9 +271,9 @@ Druid::Query.new('service/source').filter{dimension.nin(1,2,3)}
271
271
  Sometimes it can be useful to use a hash syntax for filtering for example if you already get them from a list or parameter hash.
272
272
 
273
273
  ```ruby
274
- Druid::Query.new('service/source').filter{dimension => 1, dimension1 =>2, dimension2 => 3}
274
+ Druid::Query::Builder.new.filter{dimension => 1, dimension1 =>2, dimension2 => 3}
275
275
  # which is equivalent to
276
- Druid::Query.new('service/source').filter{dimension.eq(1) & dimension1.eq(2) & dimension2.eq(3)}
276
+ Druid::Query::Builder.new.filter{dimension.eq(1) & dimension1.eq(2) & dimension2.eq(3)}
277
277
  ```
278
278
 
279
279
  ## Contributing
@@ -59,6 +59,34 @@ module Druid
59
59
  attr_accessor :byRow
60
60
  validates :byRow, allow_nil: true, inclusion: { in: [true, false] }
61
61
 
62
+ class FilterValidator < ActiveModel::EachValidator
63
+ TYPES = %w[filtered].freeze
64
+ def validate_each(record, attribute, value)
65
+ if TYPES.include?(record.type)
66
+ record.errors.add(attribute, 'may not be blank') if value.blank?
67
+ else
68
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
69
+ end
70
+ end
71
+ end
72
+
73
+ attr_accessor :filter
74
+ validates :filter, filter: true
75
+
76
+ class AggregatorValidator < ActiveModel::EachValidator
77
+ TYPES = %w[filtered].freeze
78
+ def validate_each(record, attribute, value)
79
+ if TYPES.include?(record.type)
80
+ record.errors.add(attribute, 'may not be blank') if value.blank?
81
+ else
82
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
83
+ end
84
+ end
85
+ end
86
+
87
+ attr_accessor :aggregator
88
+ validates :aggregator, aggregator: true
89
+
62
90
  def as_json(options = {})
63
91
  super(options.merge(except: %w(errors validation_context)))
64
92
  end
@@ -74,22 +74,49 @@ module Druid
74
74
  return self.post(query)
75
75
  end
76
76
 
77
- raise Error.new(response), "request failed"
77
+ raise Error.new(response)
78
78
  end
79
79
 
80
80
  MultiJson.load(response.body)
81
81
  end
82
82
 
83
83
  class Error < StandardError
84
- attr_reader :response
84
+ attr_reader :error, :error_message, :error_class, :host, :response
85
+
85
86
  def initialize(response)
86
87
  @response = response
88
+ parsed_body = MultiJson.load(response.body)
89
+ @error, @error_message, @error_class, @host = parsed_body.values_at(*%w(
90
+ error
91
+ errorMessage
92
+ errorClass
93
+ host
94
+ ))
87
95
  end
88
96
 
89
97
  def message
90
- MultiJson.load(response.body)["error"]
98
+ error
99
+ end
100
+
101
+ def query_timeout?
102
+ error == 'Query timeout'.freeze
103
+ end
104
+
105
+ def query_interrupted?
106
+ error == 'Query interrupted'.freeze
91
107
  end
92
- end
93
108
 
109
+ def query_cancelled?
110
+ error == 'Query cancelled'.freeze
111
+ end
112
+
113
+ def resource_limit_exceeded?
114
+ error == 'Resource limit exceeded'.freeze
115
+ end
116
+
117
+ def unknown_exception?
118
+ error == 'Unknown exception'.freeze
119
+ end
120
+ end
94
121
  end
95
122
  end
@@ -0,0 +1,54 @@
1
+ module Druid
2
+ class Dimension
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :type
6
+ validates :type, inclusion: { in: %w(default extraction) }
7
+
8
+ attr_accessor :dimension
9
+ validates :dimension, presence: true
10
+
11
+ attr_accessor :outputName
12
+ validates :outputName, presence: true
13
+
14
+ class ExtractionFnValidator < ActiveModel::EachValidator
15
+ TYPES = %w(extraction)
16
+ def validate_each(record, attribute, value)
17
+ if TYPES.include?(record.type)
18
+ record.errors.add(attribute, 'may not be blank') if value.blank?
19
+ else
20
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
21
+ end
22
+ end
23
+ end
24
+
25
+ attr_accessor :extractionFn
26
+ validates :extractionFn, extraction_fn: true
27
+
28
+ def initialize(params)
29
+ if params.is_a?(Hash)
30
+ super
31
+ else
32
+ super(type: 'default', dimension: params.to_s, outputName: params.to_s)
33
+ end
34
+ end
35
+
36
+ def as_json(options = {})
37
+ super(options.merge(except: %w(errors validation_context)))
38
+ end
39
+
40
+ def self.lookup(dimension, namespace, outputName: nil, retain: true, injective: false)
41
+ new({
42
+ type: 'extraction',
43
+ dimension: dimension,
44
+ outputName: outputName || namespace,
45
+ extractionFn: {
46
+ type: 'registeredLookup',
47
+ lookup: namespace,
48
+ retainMissingValue: retain,
49
+ injective: injective,
50
+ },
51
+ })
52
+ end
53
+ end
54
+ end
@@ -166,6 +166,14 @@ module Druid
166
166
  CircFilter.new(@dimension, bounds)
167
167
  end
168
168
 
169
+ def bound(params)
170
+ BoundFilter.new(@dimension, params)
171
+ end
172
+
173
+ def search(params)
174
+ SearchFilter.new(@dimension, params)
175
+ end
176
+
169
177
  def eq(value)
170
178
  case value
171
179
  when ::Array
@@ -299,7 +307,35 @@ module Druid
299
307
  @bound = {
300
308
  type: 'radius',
301
309
  coords: bounds.first,
302
- radius: bounds.last,
310
+ radius: bounds.last
311
+ }
312
+ end
313
+ end
314
+
315
+ class BoundFilter < Filter
316
+ include BooleanOperators
317
+
318
+ def initialize(dimension, params)
319
+ super()
320
+ @type = 'bound'
321
+ @dimension = dimension
322
+ @ordering = params[:ordering]
323
+ @upper = params[:upper]
324
+ @upperStrict = params[:upperStrict]
325
+ end
326
+ end
327
+
328
+ class SearchFilter < Filter
329
+ include BooleanOperators
330
+
331
+ def initialize(dimension, params)
332
+ super()
333
+ @type = 'search'
334
+ @dimension = dimension
335
+ @query = {
336
+ type: 'contains',
337
+ value: params[:value],
338
+ caseSensitive: params[:case_sensitive] || false
303
339
  }
304
340
  end
305
341
  end
@@ -5,6 +5,7 @@ require 'active_support/all'
5
5
  require 'active_model'
6
6
 
7
7
  require 'druid/granularity'
8
+ require 'druid/dimension'
8
9
  require 'druid/aggregation'
9
10
  require 'druid/post_aggregation'
10
11
  require 'druid/filter'
@@ -80,7 +81,16 @@ module Druid
80
81
  TYPES = %w(groupBy select)
81
82
  def validate_each(record, attribute, value)
82
83
  if TYPES.include?(record.queryType)
83
- record.errors.add(attribute, 'must be a list with at least one dimension') if !value.is_a?(Array) || value.blank?
84
+ if !value.is_a?(Array) || value.blank?
85
+ record.errors.add(attribute, 'must be a list with at least one dimension')
86
+ else
87
+ value.each(&:valid?) # trigger validation
88
+ value.each do |avalue|
89
+ avalue.errors.messages.each do |k, v|
90
+ record.errors.add(attribute, { k => v })
91
+ end
92
+ end
93
+ end
84
94
  else
85
95
  record.errors.add(attribute, "is not supported by type=#{record.queryType}") if value
86
96
  end
@@ -90,14 +100,34 @@ module Druid
90
100
  attr_accessor :dimensions
91
101
  validates :dimensions, dimensions: true
92
102
 
103
+ def dimensions
104
+ @dimensions ||= []
105
+ end
106
+
107
+ def dimensions=(value)
108
+ if value.is_a?(Array)
109
+ @dimensions = value.map do |x|
110
+ x.is_a?(Dimension) ? x : Dimension.new(x)
111
+ end
112
+ else
113
+ @dimensions = [
114
+ value.is_a?(Dimension) ? value : Dimension.new(value)
115
+ ]
116
+ end
117
+ end
118
+
93
119
  class AggregationsValidator < ActiveModel::EachValidator
94
120
  TYPES = %w(timeseries groupBy topN)
95
121
  def validate_each(record, attribute, value)
96
122
  if TYPES.include?(record.queryType)
97
- value.each(&:valid?) # trigger validation
98
- value.each do |avalue|
99
- avalue.errors.messages.each do |k, v|
100
- record.errors.add(attribute, { k => v })
123
+ if !value.is_a?(Array) || value.blank?
124
+ record.errors.add(attribute, 'must be a list with at least one aggregator')
125
+ else
126
+ value.each(&:valid?) # trigger validation
127
+ value.each do |avalue|
128
+ avalue.errors.messages.each do |k, v|
129
+ record.errors.add(attribute, { k => v })
130
+ end
101
131
  end
102
132
  end
103
133
  else
@@ -116,10 +146,12 @@ module Druid
116
146
  def aggregations=(value)
117
147
  if value.is_a?(Array)
118
148
  @aggregations = value.map do |x|
119
- Aggregation.new(x)
149
+ x.is_a?(Aggregation) ? x : Aggregation.new(x)
120
150
  end
121
151
  else
122
- @aggregations = [value]
152
+ @aggregations = [
153
+ value.is_a?(Aggregation) ? value : Aggregation.new(value)
154
+ ]
123
155
  end
124
156
  end
125
157
 
@@ -333,7 +365,9 @@ module Druid
333
365
 
334
366
  def group_by(*dimensions)
335
367
  query_type(:groupBy)
336
- @query.dimensions = dimensions.flatten
368
+ @query.dimensions = dimensions.flatten.map do |dimension|
369
+ dimension.is_a?(Dimension) ? dimension : Dimension.new(dimension)
370
+ end
337
371
  self
338
372
  end
339
373
 
@@ -417,6 +451,19 @@ module Druid
417
451
  self
418
452
  end
419
453
 
454
+ def filtered_aggregation(metric, name, aggregation_type, &filter)
455
+ @query.aggregations << Aggregation.new(
456
+ type: 'filtered',
457
+ filter: Filter.new.instance_exec(&filter),
458
+ aggregator: Aggregation.new(
459
+ type: aggregation_type.to_s.camelize(:lower),
460
+ name: name,
461
+ fieldName: metric
462
+ )
463
+ ) unless @query.contains_aggregation?(name)
464
+ self
465
+ end
466
+
420
467
  ## post aggregations
421
468
 
422
469
  def postagg(type = :long_sum, &block)
@@ -1,3 +1,3 @@
1
1
  module Druid
2
- VERSION = "0.9.0"
2
+ VERSION = '0.11.2'
3
3
  end
@@ -13,37 +13,37 @@ module Druid
13
13
  end
14
14
 
15
15
  def register
16
- $log.info("druid.zk register discovery path") if $log
16
+ $log.debug("druid.zk register discovery path") if $log
17
17
  @zk.on_expired_session { register }
18
18
  @zk.register(@discovery_path, only: :child) do |event|
19
- $log.info("druid.zk got event on discovery path") if $log
19
+ $log.debug("druid.zk got event on discovery path") if $log
20
20
  check_services
21
21
  end
22
22
  check_services
23
23
  end
24
24
 
25
25
  def close!
26
- $log.info("druid.zk shutting down") if $log
26
+ $log.debug("druid.zk shutting down") if $log
27
27
  @zk.close!
28
28
  end
29
29
 
30
30
  def register_service(service, brokers)
31
- $log.info("druid.zk register", service: service, brokers: brokers) if $log
31
+ $log.debug("druid.zk register", service: service, brokers: brokers) if $log
32
32
  # poor mans load balancing
33
33
  @registry[service] = brokers.shuffle
34
34
  end
35
35
 
36
36
  def unregister_service(service)
37
- $log.info("druid.zk unregister", service: service) if $log
37
+ $log.debug("druid.zk unregister", service: service) if $log
38
38
  @registry.delete(service)
39
39
  unwatch_service(service)
40
40
  end
41
41
 
42
42
  def watch_service(service)
43
43
  return if @watched_services.include?(service)
44
- $log.info("druid.zk watch", service: service) if $log
44
+ $log.debug("druid.zk watch", service: service) if $log
45
45
  watch = @zk.register(watch_path(service), only: :child) do |event|
46
- $log.info("druid.zk got event on watch path for", service: service, event: event) if $log
46
+ $log.debug("druid.zk got event on watch path for", service: service, event: event) if $log
47
47
  unwatch_service(service)
48
48
  check_service(service)
49
49
  end
@@ -52,12 +52,12 @@ module Druid
52
52
 
53
53
  def unwatch_service(service)
54
54
  return unless @watched_services.include?(service)
55
- $log.info("druid.zk unwatch", service: service) if $log
55
+ $log.debug("druid.zk unwatch", service: service) if $log
56
56
  @watched_services.delete(service).unregister
57
57
  end
58
58
 
59
59
  def check_services
60
- $log.info("druid.zk checking services") if $log
60
+ $log.debug("druid.zk checking services") if $log
61
61
  zk_services = @zk.children(@discovery_path, watch: true)
62
62
 
63
63
  (services - zk_services).each do |service|
@@ -70,7 +70,7 @@ module Druid
70
70
  end
71
71
 
72
72
  def verify_broker(service, name)
73
- $log.info("druid.zk verify", broker: name, service: service) if $log
73
+ $log.debug("druid.zk verify", broker: name, service: service) if $log
74
74
  info = @zk.get("#{watch_path(service)}/#{name}")
75
75
  node = MultiJson.load(info[0])
76
76
  uri = "http://#{node['address']}:#{node['port']}/druid/v2/"
@@ -78,7 +78,7 @@ module Druid
78
78
  method: :get, url: "#{uri}datasources/",
79
79
  timeout: 5, open_timeout: 5
80
80
  })
81
- $log.info("druid.zk verified", uri: uri, sources: check) if $log
81
+ $log.debug("druid.zk verified", uri: uri, sources: check) if $log
82
82
  return [uri, MultiJson.load(check.to_str)] if check.code == 200
83
83
  rescue
84
84
  return false
@@ -96,7 +96,7 @@ module Druid
96
96
  known = @registry[service].map { |node| node[:name] }
97
97
  live = @zk.children(watch_path(service), watch: true)
98
98
  new_list = @registry[service].select { |node| live.include?(node[:name]) }
99
- $log.info("druid.zk checking", service: service, known: known, live: live, new_list: new_list) if $log
99
+ $log.debug("druid.zk checking", service: service, known: known, live: live, new_list: new_list) if $log
100
100
 
101
101
  # verify the new entries to be living brokers
102
102
  (live - known).each do |name|
@@ -19,13 +19,13 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = Dir["spec/**/*"]
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_dependency "activesupport", "~> 4.2"
23
- spec.add_dependency "activemodel", "~> 4.2"
24
- spec.add_dependency "iso8601", "~> 0.9"
25
- spec.add_dependency "multi_json", "~> 1.12"
26
- spec.add_dependency "rest-client", "~> 2.0"
22
+ spec.add_dependency "activesupport", ">= 3.0.0"
23
+ spec.add_dependency "activemodel", ">= 3.0.0"
24
+ spec.add_dependency "iso8601", "~> 0.8"
25
+ spec.add_dependency "multi_json", "~> 1.0"
26
+ spec.add_dependency "rest-client", ">= 1.8", "< 3.0"
27
27
  spec.add_dependency "zk", "~> 1.9"
28
- spec.add_development_dependency "bundler", "~> 1.12"
28
+ spec.add_development_dependency "bundler", ">= 1.3.0", "< 2.2"
29
29
  spec.add_development_dependency "rake", "~> 11.2"
30
30
  spec.add_development_dependency "rspec", "~> 3.4"
31
31
  spec.add_development_dependency "webmock", "~> 2.1"
@@ -19,20 +19,35 @@ describe Druid::DataSource do
19
19
  end
20
20
 
21
21
  it 'raises on request failure' do
22
- # MRI
23
- stub_request(:post, 'http://www.example.com/druid/v2').
24
- with(:body => "{\"context\":{\"queryId\":null},\"queryType\":\"timeseries\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"granularity\":\"all\",\"dataSource\":\"test\"}",
25
- :headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
26
- to_return(:status => 666, :body => 'Strange server error', :headers => {})
27
- # JRuby ... *sigh
28
- stub_request(:post, 'http://www.example.com/druid/v2').
29
- with(:body => "{\"context\":{\"queryId\":null},\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"queryType\":\"timeseries\",\"dataSource\":\"test\"}",
30
- :headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
31
- to_return(:status => 666, :body => 'Strange server error', :headers => {})
22
+ stub_request(:post, 'http://www.example.com/druid/v2')
23
+ .with(
24
+ :body => %q({"context":{"queryId":null},"queryType":"timeseries","intervals":["2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00"],"granularity":"all","dataSource":"test"}),
25
+ :headers => {
26
+ 'Accept' => '*/*',
27
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
28
+ 'Content-Type' => 'application/json',
29
+ 'User-Agent' => 'Ruby',
30
+ }
31
+ )
32
+ .to_return(
33
+ :status => 500,
34
+ :body => %q({"error":"Unknown exception","errorMessage":"NullPointerException","errorClass":"java.lang.NullPointerException","host":"www.example.com"}),
35
+ :headers => {},
36
+ )
37
+
32
38
  ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2')
33
39
  query = Druid::Query::Builder.new.interval('2013-04-04', '2013-04-04').granularity(:all).query
34
40
  query.context.queryId = nil
35
- expect { ds.post(query) }.to raise_error(Druid::DataSource::Error)
41
+
42
+ expect { ds.post(query) }.to raise_error { |error|
43
+ expect(error).to be_a(Druid::DataSource::Error)
44
+ expect(error.message).to eq(error.error)
45
+ expect(error.error).to eq('Unknown exception')
46
+ expect(error.error_message).to eq('NullPointerException')
47
+ expect(error.error_class).to eq('java.lang.NullPointerException')
48
+ expect(error.host).to eq('www.example.com')
49
+ expect(error).to be_unknown_exception
50
+ }
36
51
  end
37
52
  end
38
53
 
@@ -21,7 +21,9 @@ describe Druid::Query do
21
21
 
22
22
  it 'takes dimensions from group_by method' do
23
23
  @query.group_by(:a, :b, :c)
24
- expect(JSON.parse(@query.query.to_json)['dimensions']).to eq(['a', 'b', 'c'])
24
+ expect(JSON.parse(@query.query.to_json)['dimensions']).to eq([{"type"=>"default", "dimension"=>"a", "outputName"=>"a"},
25
+ {"type"=>"default", "dimension"=>"b", "outputName"=>"b"},
26
+ {"type"=>"default", "dimension"=>"c", "outputName"=>"c"}])
25
27
  end
26
28
 
27
29
  it 'takes dimension, metric and threshold from topn method' do
@@ -207,6 +209,32 @@ describe Druid::Query do
207
209
  end
208
210
  end
209
211
 
212
+ describe '#filtered_aggregation' do
213
+ it 'builds filtered aggregations' do
214
+ @query.filtered_aggregation(:a, :a_filtered, :longSum) do
215
+ b.eq(2) & c.neq(3)
216
+ end
217
+ expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [
218
+ {
219
+ 'type' => 'filtered',
220
+ 'filter' => {
221
+ 'type' => 'and',
222
+ 'fields' => [
223
+ { 'dimension' => 'b', 'type' => 'selector', 'value' => 2 },
224
+ {
225
+ 'type' => 'not',
226
+ 'field' => {
227
+ 'dimension' => 'c', 'type' => 'selector', 'value' => 3
228
+ }
229
+ }
230
+ ]
231
+ },
232
+ 'aggregator' => { 'type' => 'longSum', 'name' => 'a_filtered', 'fieldName' => 'a' }
233
+ }
234
+ ]
235
+ end
236
+ end
237
+
210
238
  it 'appends long_sum properties from aggregations on calling long_sum again' do
211
239
  @query.long_sum(:a, :b, :c)
212
240
  @query.double_sum(:x,:y)
metadata CHANGED
@@ -1,85 +1,91 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-druid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruby Druid Community
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-18 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: 3.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.2'
26
+ version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activemodel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '4.2'
33
+ version: 3.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '4.2'
40
+ version: 3.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: iso8601
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.9'
47
+ version: '0.8'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.9'
54
+ version: '0.8'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: multi_json
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.12'
61
+ version: '1.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.12'
68
+ version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rest-client
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1.8'
76
+ - - "<"
74
77
  - !ruby/object:Gem::Version
75
- version: '2.0'
78
+ version: '3.0'
76
79
  type: :runtime
77
80
  prerelease: false
78
81
  version_requirements: !ruby/object:Gem::Requirement
79
82
  requirements:
80
- - - "~>"
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '1.8'
86
+ - - "<"
81
87
  - !ruby/object:Gem::Version
82
- version: '2.0'
88
+ version: '3.0'
83
89
  - !ruby/object:Gem::Dependency
84
90
  name: zk
85
91
  requirement: !ruby/object:Gem::Requirement
@@ -98,16 +104,22 @@ dependencies:
98
104
  name: bundler
99
105
  requirement: !ruby/object:Gem::Requirement
100
106
  requirements:
101
- - - "~>"
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.3.0
110
+ - - "<"
102
111
  - !ruby/object:Gem::Version
103
- version: '1.12'
112
+ version: '2.2'
104
113
  type: :development
105
114
  prerelease: false
106
115
  version_requirements: !ruby/object:Gem::Requirement
107
116
  requirements:
108
- - - "~>"
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 1.3.0
120
+ - - "<"
109
121
  - !ruby/object:Gem::Version
110
- version: '1.12'
122
+ version: '2.2'
111
123
  - !ruby/object:Gem::Dependency
112
124
  name: rake
113
125
  requirement: !ruby/object:Gem::Requirement
@@ -153,7 +165,7 @@ dependencies:
153
165
  description: |2
154
166
  ruby-druid is a Ruby client for Druid. It includes a Squeel-like query DSL
155
167
  and generates a JSON query that can be sent to Druid directly.
156
- email:
168
+ email:
157
169
  executables: []
158
170
  extensions: []
159
171
  extra_rdoc_files: []
@@ -165,6 +177,7 @@ files:
165
177
  - lib/druid/client.rb
166
178
  - lib/druid/context.rb
167
179
  - lib/druid/data_source.rb
180
+ - lib/druid/dimension.rb
168
181
  - lib/druid/filter.rb
169
182
  - lib/druid/granularity.rb
170
183
  - lib/druid/having.rb
@@ -182,7 +195,7 @@ homepage: https://github.com/ruby-druid/ruby-druid
182
195
  licenses:
183
196
  - MIT
184
197
  metadata: {}
185
- post_install_message:
198
+ post_install_message:
186
199
  rdoc_options: []
187
200
  require_paths:
188
201
  - lib
@@ -197,14 +210,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
210
  - !ruby/object:Gem::Version
198
211
  version: '0'
199
212
  requirements: []
200
- rubyforge_project:
201
- rubygems_version: 2.5.1
202
- signing_key:
213
+ rubygems_version: 3.0.3
214
+ signing_key:
203
215
  specification_version: 4
204
216
  summary: A Ruby client for Druid
205
217
  test_files:
218
+ - spec/spec_helper.rb
206
219
  - spec/lib/client_spec.rb
220
+ - spec/lib/zk_spec.rb
207
221
  - spec/lib/data_source_spec.rb
208
222
  - spec/lib/query_spec.rb
209
- - spec/lib/zk_spec.rb
210
- - spec/spec_helper.rb