influxer 0.0.1 → 0.1.0

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.
@@ -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