influxer 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,72 @@
1
+ require 'influxer/metrics/relation/time_query'
2
+ require 'influxer/metrics/relation/fanout_query'
3
+
1
4
  module Influxer
2
5
  class Relation
6
+ attr_reader :values
7
+
8
+ include Influxer::TimeQuery
9
+ include Influxer::FanoutQuery
3
10
 
11
+ MULTI_VALUE_METHODS = [:select, :where, :group]
12
+
13
+ MULTI_KEY_METHODS = [:fanout]
14
+
15
+ SINGLE_VALUE_METHODS = [:fill, :limit, :merge, :time]
16
+
17
+ MULTI_VALUE_SIMPLE_METHODS = [:select, :group]
18
+
19
+ SINGLE_VALUE_SIMPLE_METHODS = [:fill, :limit, :merge]
20
+
21
+ MULTI_VALUE_METHODS.each do |name|
22
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
23
+ def #{name}_values # def select_values
24
+ @values[:#{name}] ||= [] # @values[:select] || []
25
+ end # end
26
+ CODE
27
+ end
28
+
29
+ MULTI_KEY_METHODS.each do |name|
30
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
31
+ def #{name}_values # def fanout_values
32
+ @values[:#{name}] ||= {} # @values[:fanout] || {}
33
+ end # end
34
+ CODE
35
+ end
36
+
37
+ SINGLE_VALUE_METHODS.each do |name|
38
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
39
+ def #{name}_value # def limit_value
40
+ @values[:#{name}] # @values[:limit]
41
+ end # end
42
+ CODE
43
+ end
44
+
45
+ SINGLE_VALUE_SIMPLE_METHODS.each do |name|
46
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
47
+ def #{name}(val) # def limit(val)
48
+ @values[:#{name}] = val # @value[:limit] = val
49
+ self # self
50
+ end # end
51
+ CODE
52
+ end
53
+
54
+ MULTI_VALUE_SIMPLE_METHODS.each do |name|
55
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
56
+ def #{name}(*args) # def select(*args)
57
+ #{name}_values.concat args.map(&:to_s) # select_values.concat args.map(&:to_s)
58
+ self # self
59
+ end # end
60
+ CODE
61
+ end
62
+
4
63
  # Initialize new Relation for 'klass' (Class) metrics.
5
64
  #
6
65
  # Available params:
7
66
  # :attributes - hash of attributes to be included to new Metrics object and where clause of Relation
8
67
  #
9
68
  def initialize(klass, params = {})
69
+ @klass = klass
10
70
  @instance = klass.new params[:attributes]
11
71
  self.reset
12
72
  self.where(params[:attributes]) if params[:attributes].present?
@@ -26,59 +86,46 @@ module Influxer
26
86
  @instance
27
87
  end
28
88
 
29
- # accepts strings and symbols only
30
- def select(*args)
31
- return self if args.empty?
32
- @select_values.concat args
33
- self
34
- end
35
-
36
89
  # accepts hash or strings conditions
37
- # TODO: add sanitization and array support
38
-
39
90
  def where(*args,**hargs)
40
- @where_values.concat args.map{|str| "(#{str})"}
41
-
42
- unless hargs.empty?
43
- hargs.each do |key, val|
44
- @where_values << "(#{key}=#{quoted(val)})"
45
- end
46
- end
47
- self
48
- end
49
-
50
- def group(*args)
51
- return self if args.empty?
52
- @group_values.concat args
91
+ build_where(args, hargs, false)
53
92
  self
54
93
  end
55
94
 
56
- def limit(val)
57
- @limit = val
95
+ def not(*args, **hargs)
96
+ build_where(args, hargs, true)
58
97
  self
59
98
  end
60
99
 
61
100
  def to_sql
62
101
  sql = ["select"]
63
102
 
64
- if @select_values.empty?
103
+ if select_values.empty?
65
104
  sql << "*"
66
105
  else
67
- sql << @select_values.join(",")
106
+ sql << select_values.uniq.join(",")
68
107
  end
69
108
 
70
- sql << "from #{@instance.series}"
109
+ sql << "from #{ build_series_name }"
110
+
111
+ unless merge_value.nil?
112
+ sql << "merge #{ @instance.quote_series(merge_value) }"
113
+ end
114
+
115
+ unless group_values.empty? and time_value.nil?
116
+ sql << "group by #{ (time_value.nil? ? [] : ['time('+@values[:time]+')']).concat(group_values).uniq.join(",") }"
117
+ end
71
118
 
72
- unless @group_values.empty?
73
- sql << "group by #{@group_values.join(",")}"
119
+ unless fill_value.nil?
120
+ sql << "fill(#{ fill_value })"
74
121
  end
75
122
 
76
- unless @where_values.empty?
77
- sql << "where #{@where_values.join(" and ")}"
123
+ unless where_values.empty?
124
+ sql << "where #{ where_values.join(" and ") }"
78
125
  end
79
126
 
80
- unless @limit.nil?
81
- sql << "limit #{@limit}"
127
+ unless limit_value.nil?
128
+ sql << "limit #{ limit_value }"
82
129
  end
83
130
  sql.join " "
84
131
  end
@@ -101,12 +148,96 @@ module Influxer
101
148
  end
102
149
 
103
150
  def delete_all
151
+ sql = ["delete"]
152
+
153
+ sql << "from #{@instance.series}"
154
+
155
+ unless where_values.empty?
156
+ sql << "where #{where_values.join(" and ")}"
157
+ end
158
+
159
+ sql = sql.join " "
160
+
161
+ @instance.client.query sql
162
+ end
163
+
164
+ def scoping
165
+ previous, @klass.current_scope = @klass.current_scope, self
166
+ yield
167
+ ensure
168
+ @klass.current_scope = previous
169
+ end
170
+
171
+ def merge!(rel)
172
+ return self if rel.nil?
173
+ MULTI_VALUE_METHODS.each do |method|
174
+ (@values[method]||=[]).concat(rel.values[method]).uniq! unless rel.values[method].nil?
175
+ end
176
+
177
+ MULTI_KEY_METHODS.each do |method|
178
+ (@values[method]||={}).merge!(rel.values[method]) unless rel.values[method].nil?
179
+ end
180
+
181
+ SINGLE_VALUE_METHODS.each do |method|
182
+ @values[method] = rel.values[method] unless rel.values[method].nil?
183
+ end
104
184
 
185
+ self
105
186
  end
106
187
 
107
188
  protected
189
+ def build_where(args, hargs, negate)
190
+ case
191
+ when (args.present? and args[0].is_a?(String))
192
+ where_values.concat args.map{|str| "(#{str})"}
193
+ when hargs.present?
194
+ build_hash_where(hargs, negate)
195
+ else
196
+ false
197
+ end
198
+ end
199
+
200
+ def build_hash_where(hargs, negate = false)
201
+ hargs.each do |key, val|
202
+ if @klass.fanout?(key)
203
+ build_fanout(key,val)
204
+ else
205
+ where_values << "(#{ build_eql(key,val,negate) })"
206
+ end
207
+ end
208
+ end
209
+
210
+ def build_eql(key,val,negate)
211
+ case val
212
+ when Regexp
213
+ "#{key}#{ negate ? '!~' : '=~'}#{val.inspect}"
214
+ when Array
215
+ build_in(key,val,negate)
216
+ when Range
217
+ build_range(key,val,negate)
218
+ else
219
+ "#{key}#{ negate ? '<>' : '='}#{quoted(val)}"
220
+ end
221
+ end
222
+
223
+ def build_in(key, arr, negate)
224
+ buf = []
225
+ arr.each do |val|
226
+ buf << build_eql(key,val,negate)
227
+ end
228
+ "#{ buf.join( negate ? ' and ' : ' or ') }"
229
+ end
230
+
231
+ def build_range(key,val,negate)
232
+ unless negate
233
+ "#{key}>#{quoted(val.begin)} and #{key}<#{quoted(val.end)}"
234
+ else
235
+ "#{key}<#{quoted(val.begin)} and #{key}>#{quoted(val.end)}"
236
+ end
237
+ end
238
+
108
239
  def load
109
- @records = @instance.client.query to_sql
240
+ @records = get_points(@instance.client.cached_query(to_sql))
110
241
  @loaded = true
111
242
  end
112
243
 
@@ -115,10 +246,7 @@ module Influxer
115
246
  end
116
247
 
117
248
  def reset
118
- @limit = nil
119
- @select_values = []
120
- @group_values = []
121
- @where_values = []
249
+ @values = {}
122
250
  @records = nil
123
251
  @loaded = false
124
252
  self
@@ -131,7 +259,7 @@ module Influxer
131
259
  end
132
260
 
133
261
  def quoted(val)
134
- if val.is_a?(String)
262
+ if val.is_a?(String) or val.is_a?(Symbol)
135
263
  "'#{val}'"
136
264
  elsif val.kind_of?(Time) or val.kind_of?(DateTime)
137
265
  "#{val.to_i}s"
@@ -139,5 +267,15 @@ module Influxer
139
267
  val.to_s
140
268
  end
141
269
  end
270
+
271
+ def method_missing(method, *args, &block)
272
+ if @klass.respond_to?(method)
273
+ merge!(scoping { @klass.public_send(method, *args, &block) })
274
+ end
275
+ end
276
+
277
+ def get_points(hash)
278
+ hash.values.reduce([],:+)
279
+ end
142
280
  end
143
281
  end
@@ -0,0 +1,28 @@
1
+ module Influxer
2
+ module Scoping
3
+ module Default
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :default_scopes
8
+ self.default_scopes = []
9
+ end
10
+
11
+ module ClassMethods
12
+ def default_scope(scope)
13
+ self.default_scopes += [scope] unless scope.nil?
14
+ end
15
+
16
+ def unscoped
17
+ Relation.new self
18
+ end
19
+
20
+ def default_scoped
21
+ self.default_scopes.inject(Relation.new(self)) do |rel, scope|
22
+ rel.merge!(rel.scoping{ scope.call })
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module Influxer
2
+ module Scoping
3
+ module Named
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def scope(name, scope)
8
+ raise Error.new("Scope not defined: #{name}") if scope.nil? or !scope.respond_to?(:call)
9
+ singleton_class.send(:define_method, name) do |*args|
10
+ rel = all
11
+ rel.merge!(rel.scoping { scope.call(*args) })
12
+ rel
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_support/per_thread_registry'
2
+ require 'influxer/metrics/scoping/default'
3
+ require 'influxer/metrics/scoping/named'
4
+
5
+ module Influxer
6
+ module Scoping
7
+ extend ActiveSupport::Concern
8
+
9
+ class Error < StandardError; end;
10
+
11
+ included do
12
+ include Default
13
+ include Named
14
+ end
15
+
16
+ module ClassMethods
17
+ def current_scope #:nodoc:
18
+ ScopeRegistry.value_for(:current_scope, name)
19
+ end
20
+
21
+ def current_scope=(scope) #:nodoc:
22
+ ScopeRegistry.set_value_for(:current_scope, name, scope)
23
+ end
24
+ end
25
+
26
+ class ScopeRegistry # :nodoc:
27
+ extend ActiveSupport::PerThreadRegistry
28
+
29
+ VALID_SCOPE_TYPES = [:current_scope]
30
+
31
+ def initialize
32
+ @registry = Hash.new { |hash, key| hash[key] = {} }
33
+ end
34
+
35
+ # Obtains the value for a given +scope_name+ and +variable_name+.
36
+ def value_for(scope_type, variable_name)
37
+ raise_invalid_scope_type!(scope_type)
38
+ @registry[scope_type][variable_name]
39
+ end
40
+
41
+ # Sets the +value+ for a given +scope_type+ and +variable_name+.
42
+ def set_value_for(scope_type, variable_name, value)
43
+ raise_invalid_scope_type!(scope_type)
44
+ @registry[scope_type][variable_name] = value
45
+ end
46
+
47
+ private
48
+
49
+ def raise_invalid_scope_type!(scope_type)
50
+ if !VALID_SCOPE_TYPES.include?(scope_type)
51
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -17,8 +17,10 @@ module Influxer
17
17
  attrs = params[:inherits]
18
18
  end
19
19
 
20
+ _foreign_key = params.key?(:foreign_key) ? params[:foreign_key] : self.to_s.foreign_key
21
+
20
22
  define_method(metrics_name) do
21
- rel_attrs = {self.class.to_s.foreign_key => self.id}
23
+ rel_attrs = _foreign_key ? {_foreign_key => self.id} : {}
22
24
 
23
25
  unless attrs.nil?
24
26
  attrs.each do |key|
@@ -1,3 +1,3 @@
1
1
  module Influxer
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/spec/client_spec.rb CHANGED
@@ -2,6 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  describe Influxer::Client do
4
4
 
5
+ after(:each) do
6
+ Rails.cache.clear
7
+ end
8
+
5
9
  let(:conf) { Influxer.config }
6
10
  let(:client) { Influxer.client }
7
11
 
@@ -11,4 +15,34 @@ describe Influxer::Client do
11
15
  expect(client.database).to eq conf.database
12
16
  end
13
17
 
18
+ describe "cache" do
19
+ before do
20
+ allow_any_instance_of(Influxer::Client).to receive(:query) do |_, sql|
21
+ sql
22
+ end
23
+ end
24
+
25
+ let(:q) { "list series" }
26
+
27
+ after(:each) do
28
+ conf.cache = false
29
+ end
30
+
31
+ it "should write data to cache" do
32
+ conf.cache = {}
33
+
34
+ client.cached_query(q)
35
+ expect(Rails.cache.exist?("influxer:listseries")).to be_truthy
36
+ end
37
+
38
+ it "should write data to cache with expiration" do
39
+ conf.cache = {expires_in: 1}
40
+
41
+ client.cached_query(q)
42
+ expect(Rails.cache.exist?("influxer:listseries")).to be_truthy
43
+
44
+ sleep 2
45
+ expect(Rails.cache.exist?("influxer:listseries")).to be_falsey
46
+ end
47
+ end
14
48
  end
@@ -1,3 +1,3 @@
1
1
  class TestoMetrics < Influxer::Metrics
2
- attributes :testo_id, :receipt_id
2
+ attributes :testo_id, :receipt_id, :testo
3
3
  end
@@ -1,4 +1,6 @@
1
1
  class Testo < ActiveRecord::Base
2
2
  has_metrics
3
3
  has_metrics :testo_metrics, class_name: "TestoMetrics", inherits: [:receipt_id]
4
+ has_metrics :testo2_metrics, class_name: "TestoMetrics", foreign_key: :testo
5
+ has_metrics :custom_metrics, class_name: "TestoMetrics", foreign_key: nil
4
6
  end
@@ -7,6 +7,7 @@ require "influxer"
7
7
 
8
8
  module Dummy
9
9
  class Application < Rails::Application
10
+ config.cache_store = :memory_store
10
11
  # Settings in config/environments/* take precedence over those specified here.
11
12
  # Application configuration should go into files in config/initializers
12
13
  # -- all .rb files in that directory are automatically loaded.
@@ -1,6 +1,5 @@
1
1
  Dummy::Application.configure do
2
2
  # Settings specified here will take precedence over those in config/application.rb.
3
-
4
3
  # The test environment is used exclusively to run your application's
5
4
  # test suite. You never need to work with it otherwise. Remember that
6
5
  # your test database is "scratch space" for the test suite and is wiped
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Influxer::Metrics do
4
+ before do
5
+ allow_any_instance_of(Influxer::Client).to receive(:query) do |_, sql|
6
+ sql
7
+ end
8
+ end
9
+
10
+ let(:dummy) do
11
+ Class.new(Influxer::Metrics) do
12
+ set_series 'dummy'
13
+ default_scope -> { time(:hour) }
14
+ end
15
+ end
16
+
17
+ let(:doomy) do
18
+ Class.new(dummy) do
19
+ scope :by_user, -> (id) { where(user: id) if id.present? }
20
+ scope :hourly, -> { where(by: :hour).time(nil) }
21
+ scope :daily, -> { where(by: :day).time(nil) }
22
+
23
+ fanout :by, :user, :account, delimeter: "."
24
+ end
25
+ end
26
+
27
+ let(:dappy) do
28
+ Class.new(doomy) do
29
+ fanout :user, delimeter: "_"
30
+ end
31
+ end
32
+
33
+ describe "fanouts" do
34
+ it "should work with one fanout" do
35
+ expect(doomy.by_user(1).to_sql).to eq "select * from \"dummy.user.1\" group by time(1h)"
36
+ end
37
+
38
+ it "should work with several fanouts" do
39
+ expect(dappy.by_user(1).hourly.to_sql).to eq "select * from \"dummy_by_hour_user_1\""
40
+ end
41
+
42
+ it "should work with regexp fanouts" do
43
+ expect(dappy.where(dummy_id: 100).by_user(/[1-3]/).daily.to_sql).to eq "select * from merge(/^dummy_by_day_user_[1-3]$/) where (dummy_id=100)"
44
+ end
45
+ end
46
+ end
@@ -1,6 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Influxer::Metrics do
4
+ before do
5
+ allow_any_instance_of(Influxer::Client).to receive(:query) do |_, sql|
6
+ sql
7
+ end
8
+ end
4
9
 
5
10
  let(:metrics) { Influxer::Metrics.new }
6
11
  let(:metrics_class) { Influxer::Metrics }
@@ -16,7 +21,10 @@ describe Influxer::Metrics do
16
21
 
17
22
  specify { expect(metrics_class).to respond_to :all}
18
23
  specify { expect(metrics_class).to respond_to :where}
19
- specify { expect(metrics_class).to respond_to :group}
24
+ specify { expect(metrics_class).to respond_to :merge}
25
+ specify { expect(metrics_class).to respond_to :time}
26
+ specify { expect(metrics_class).to respond_to :past}
27
+ specify { expect(metrics_class).to respond_to :since}
20
28
  specify { expect(metrics_class).to respond_to :limit}
21
29
  specify { expect(metrics_class).to respond_to :select}
22
30
  specify { expect(metrics_class).to respond_to :delete_all}
@@ -86,12 +94,30 @@ describe Influxer::Metrics do
86
94
  end
87
95
  end
88
96
 
97
+ let(:dummy_metrics_2) do
98
+ Class.new(Influxer::Metrics) do
99
+ set_series "dummy \"A\""
100
+ end
101
+ end
102
+
103
+ let(:dummy_metrics_3) do
104
+ Class.new(Influxer::Metrics) do
105
+ set_series /^.*$/
106
+ end
107
+ end
108
+
89
109
  let(:dummy_with_2_series) do
90
110
  Class.new(Influxer::Metrics) do
91
111
  set_series :events, :errors
92
112
  end
93
113
  end
94
114
 
115
+ let(:dummy_with_2_series_quoted) do
116
+ Class.new(Influxer::Metrics) do
117
+ set_series "dummy \"A\"", "dummy \"B\""
118
+ end
119
+ end
120
+
95
121
  let(:dummy_with_proc_series) do
96
122
  Class.new(Influxer::Metrics) do
97
123
  attributes :user_id, :test_id
@@ -99,27 +125,36 @@ describe Influxer::Metrics do
99
125
  end
100
126
  end
101
127
 
102
-
103
-
104
128
  describe "set_series" do
105
129
  it "should set series name from class name by default" do
106
- expect(DummyMetrics.series).to eq 'dummy'
130
+ expect(DummyMetrics.new.series).to eq "\"dummy\""
107
131
  end
108
132
 
109
133
  it "should set series from subclass" do
110
- expect(dummy_metrics.series).to eq 'dummies'
134
+ expect(dummy_metrics.new.series).to eq "\"dummies\""
135
+ end
136
+
137
+ it "should set series as regexp" do
138
+ expect(dummy_metrics_3.new.series).to eq '/^.*$/'
139
+ end
140
+
141
+ it "should set series with quotes" do
142
+ expect(dummy_metrics_2.new.series).to eq "\"dummy \\\"A\\\"\""
111
143
  end
112
144
 
113
145
  it "should set several series" do
114
- expect(dummy_with_2_series.series).to eq 'events,errors'
146
+ expect(dummy_with_2_series.new.series).to eq "merge(\"events\",\"errors\")"
115
147
  end
116
148
 
149
+ it "should set several series with quotes" do
150
+ expect(dummy_with_2_series_quoted.new.series).to eq "merge(\"dummy \\\"A\\\"\",\"dummy \\\"B\\\"\")"
151
+ end
117
152
 
118
153
  it "should set series from proc" do
119
154
  expect(dummy_with_proc_series.series).to be_an_instance_of Proc
120
155
 
121
156
  m = dummy_with_proc_series.new user_id: 2, test_id:123
122
- expect(dummy_with_proc_series.series.call(m)).to eq "test/123/user/2"
157
+ expect(m.series).to eq "\"test/123/user/2\""
123
158
  end
124
159
  end
125
160