influxer 0.0.1 → 0.1.0

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
2
  SHA1:
3
- metadata.gz: f441918c927f139e08eaa0e07e66171c130b5f35
4
- data.tar.gz: c26b9d64e124409ae490d391457010497b6dcea9
3
+ metadata.gz: 3bb445897c491a860d94249f174bc02e9ad6b61f
4
+ data.tar.gz: 011778f550be687a6d1503618f98ec70dd454c2a
5
5
  SHA512:
6
- metadata.gz: 942b9ff9c5960df2ee79188431373b24575d0cf5965136631fc993edbfbe7635adc6f321a15464a6a3904eed169f843bb4480af7d15e8ee3aecd9774144b69af
7
- data.tar.gz: 63662390624e4c40b3c6cb14ee103157a6b30f27c15a1c7de6bc4354c7f0dd045b27386998d6a27510de02cd730fca1d4b4726d83071ffd879740d9995e335b9
6
+ metadata.gz: 85e6922159fc728186d5b3a0736ff1c9950f1af0a1d70f8452e91f6e1759e2508a1e1566d9f55482ffefe62a01b39d2843aea81aec8e5382217e015291b26dd5
7
+ data.tar.gz: 232e5a9108e36d1378134a1e26fbc9d25367a569cae4a3f7ab752c2c48f3da5ea47e395019032fd2896cc6ed90421c6e8b07283b94690453e2d8ec120b0fe144
data/.gitignore CHANGED
@@ -33,5 +33,4 @@ spec/dummy/tmp/
33
33
 
34
34
  Gemfile.lock
35
35
  .rspec
36
-
37
-
36
+ *.gem
data/.travis.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
4
  - 2.0.0
4
5
  - 2.1
@@ -14,8 +15,8 @@ matrix:
14
15
  - rvm: 2.1
15
16
  gemfile: gemfiles/rails40.gemfile
16
17
 
17
- - rvm: 2.0.0
18
+ - rvm: 2.1
18
19
  gemfile: gemfiles/rails41.gemfile
19
20
 
20
21
  - rvm: 2.1
21
- gemfile: gemfiles/rails41.gemfile
22
+ gemfile: gemfiles/rails42.gemfile
data/Changelog.md ADDED
@@ -0,0 +1,19 @@
1
+ ## 0.1.0
2
+ - Add logs
3
+ - Add `foreign_key` param to `has_metrics` options
4
+
5
+ ## 0.1.0-rc
6
+ - Fix `Relation#to_a` (now returns array of points correctrly)
7
+ - Fix fanout queries with array args (now use `merge(Regexp)`)
8
+
9
+ ## 0.1.0-alpha
10
+ - Add `time` method to Relation to group by time with constants (`:hour`, `:day`, etc) and fill support
11
+ - Series names now properly quoted with double-quotes
12
+ - Using regexps, ranges and arrays within `where` clause
13
+ - `where.not(...)` support
14
+ - Add `past` and `since` methods
15
+ - Add `merge` method and support for regexp series
16
+ - Add `delete_all` support
17
+ - Add cache support (using `Rails.cache`)
18
+ - Scopes (default and named)
19
+ - Support for fanout series
data/Gemfile CHANGED
@@ -1,16 +1,3 @@
1
1
  source "https://rubygems.org"
2
-
3
- # Declare your gem's dependencies in influxer.gemspec.
4
- # Bundler will treat runtime dependencies like base dependencies, and
5
- # development dependencies will be added by default to the :development group.
6
- gem 'sqlite3'
7
- gem 'pry-byebug'
8
2
  gemspec
9
3
 
10
- # Declare any dependencies that are still in development here instead of in
11
- # your gemspec. These might include edge Rails or gems from your path or
12
- # Git. Remember to move these dependencies to your gemspec before releasing
13
- # your gem to rubygems.org.
14
-
15
- # To use debugger
16
- # gem 'debugger'
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2014 YOURNAME
1
+ Copyright 2014 palkan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -2,14 +2,59 @@
2
2
 
3
3
  ## Influxer
4
4
 
5
- Use InfluxDB in your Rails (>4.0) application.
5
+ ActiveRecord-like wrapper for [influxdb-ruby](https://github.com/influxdb/influxdb-ruby) with many useful features, such as:
6
+ - Familar query language (use `select`, `where`, `not`, `group` etc).
7
+ - Support for Regex conditions: `where(page_id: /^home\/.*/) #=> select * ... where page_id=~/^home\/.*/`.
8
+ - Special query methods for InfluxDB:
9
+ - `time` - group by time (e.g. `Metrics.time(:hour) => # select * ... group by time(1h)`);
10
+ - `past` - get only points for last hour/minute/whatever (e.g. `Metrics.past(:day) => # select * ... where time > now() - 1d`);
11
+ - `since` - get only points since date (e.g. `Metrics.since(Time.utc(2014,12,31)) => # select * ... where time > 1419984000s`);
12
+ - `merge` - merge series.
13
+ - Scopes support
14
+ ```
15
+ class Metrics < Influxer::Metrics
16
+ default_scope -> { time(:hour).limit(1000) }
17
+ scope :unlimited, -> { limit(nil) }
18
+ scope :by_account, -> (id) { where(account_id: id) if id.present? }
19
+ end
6
20
 
7
- ### Basic support
21
+ Metrics.by_account(1)
22
+ # => select * from "metrics" group by time(1h) where account_id=1 limit 1000
8
23
 
9
- ToDo
24
+ Metrics.unlimited.by_account(1).time(:week)
25
+ # => select * from "metrics" group by time(1w) where account_id=1
10
26
 
11
- ### ActiveRecord
27
+ ```
28
+ - Support for handling fanout series as one metrics.
29
+ ```
30
+ class Metrics < Influxer::Metrics
31
+ fanout :account, :user, :page
32
+ end
12
33
 
13
- `has_metrics`
34
+ Metrics.where(account: 1)
35
+ # => select * from "metrics_account_1"
14
36
 
15
- ToDo
37
+
38
+ Metrics.where(page: 'home').where(user: 12)
39
+ # => select * from "metrics_user_12_page_home"
40
+
41
+ Metrics.where(page: /(home|faq)/).where(account: 1).where(user: 12)
42
+ # => select * from /^metrics_account_1_user_12_page_(home|faq)$/
43
+
44
+ ```
45
+ - Integrate with your model:
46
+ ```
47
+ class UserVisits < Influxer::Metrics
48
+ end
49
+
50
+ class User < ActiveRecord::Base
51
+ has_metrics :visits
52
+ end
53
+
54
+ user = User.find(1)
55
+ user.visits.write(page_id: 'home')
56
+ #=> < creates point {user_id: 1, page_id: 'home'} in 'user_visits' series >
57
+
58
+ user.visits.where(page_id: 'home')
59
+ #=> select * from user_visits where page_id='home'
60
+ ```
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sqlite3', platform: :mri
4
+ gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
5
+ gem 'rails', '~> 4.2.0'
6
+
7
+ gemspec path: '..'
data/influxer.gemspec CHANGED
@@ -1,16 +1,14 @@
1
1
  $:.push File.expand_path("../lib", __FILE__)
2
2
 
3
- # Maintain your gem's version:
4
3
  require "influxer/version"
5
4
 
6
- # Describe your gem and declare its dependencies:
7
5
  Gem::Specification.new do |s|
8
6
  s.name = "influxer"
9
7
  s.version = Influxer::VERSION
10
8
  s.authors = ["Vlad Dem"]
11
9
  s.email = ["dementiev.vm@gmail.com"]
12
10
  s.homepage = "http://github.com/palkan/influxer"
13
- s.summary = "InfluxDB support for Rails"
11
+ s.summary = "InfluxDB for Rails"
14
12
  s.description = "InfluxDB the Rails way"
15
13
  s.license = "MIT"
16
14
 
@@ -22,6 +20,10 @@ Gem::Specification.new do |s|
22
20
 
23
21
  s.add_development_dependency "timecop"
24
22
  s.add_development_dependency "simplecov", ">= 0.3.8"
25
- s.add_development_dependency "rspec", "~> 3.0.0"
26
- s.add_development_dependency "rspec-rails", "~> 3.0.0"
23
+
24
+ s.add_development_dependency 'sqlite3'
25
+ s.add_development_dependency 'pry'
26
+ s.add_development_dependency 'pry-byebug'
27
+ s.add_development_dependency "rspec", "~> 3.1.0"
28
+ s.add_development_dependency "rspec-rails", "~> 3.1.0"
27
29
  end
@@ -3,7 +3,53 @@ require 'influxdb'
3
3
  module Influxer
4
4
  class Client < ::InfluxDB::Client
5
5
  def initialize
6
+ @instrumenter = ActiveSupport::Notifications.instrumenter
6
7
  super Influxer.config.database, Influxer.config.as_json.symbolize_keys!
7
8
  end
9
+
10
+ def cached_query(sql)
11
+ log(sql) do
12
+ unless Influxer.config.cache == false
13
+ Rails.cache.fetch(normalized_cache_key(sql), cache_options(sql)) { self.query(sql) }
14
+ else
15
+ self.query(sql)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def log(sql)
23
+ return yield unless logger.debug?
24
+
25
+ _start = Time.now
26
+ res = yield
27
+ _duration = (Time.now - _start)*1000
28
+
29
+ name = "InfluxDB SQL (#{_duration.round(1)}ms)"
30
+
31
+ # bold black name and blue query string
32
+ msg = "\e[1m\e[30m#{name}\e[0m \e[34m#{sql}\e[0m"
33
+ logger.debug msg
34
+ res
35
+ end
36
+
37
+ def cache_options(sql=nil)
38
+ options = Influxer.config.cache.dup
39
+ # if sql contains 'now()' set expires to 1 minute or :cache_now_for value of config.cache if defined
40
+ if sql =~ /\snow\(\)/
41
+ options[:expires_in] = options[:cache_now_for] || 60
42
+ end
43
+ options
44
+ end
45
+
46
+ # add prefix; remove whitespaces
47
+ def normalized_cache_key(sql)
48
+ "influxer:#{sql.gsub(/\s*/, '')}"
49
+ end
50
+
51
+ def logger
52
+ Rails.logger
53
+ end
8
54
  end
9
55
  end
@@ -7,6 +7,7 @@ module Influxer
7
7
  :password,
8
8
  :use_ssl,
9
9
  :async,
10
+ :cache,
10
11
  :retry,
11
12
  :time_precision,
12
13
  :initial_delay,
@@ -20,6 +21,7 @@ module Influxer
20
21
  @port = 8083
21
22
  @use_ssl = false
22
23
  @async = true
24
+ @cache = false
23
25
  @retry = false
24
26
  @time_precision = 's'
25
27
  @max_delay = 30
@@ -42,6 +44,14 @@ module Influxer
42
44
  config.each do |key, val|
43
45
  self.send("#{key}=",val)
44
46
  end
47
+
48
+ # we want pass @cache value as options to cache store, so we want it to be a Hash
49
+ if @cache == true
50
+ @cache = {}
51
+ elsif @cache != false
52
+ # cache keys should be symbols to work as Rails.cache options
53
+ @cache.symbolize_keys!
54
+ end
45
55
  end
46
56
  end
47
57
  end
@@ -0,0 +1,53 @@
1
+ module Influxer
2
+ module Fanout
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :fanouts, :fanouts_by_name, :fanout_options
7
+ self.fanouts = []
8
+ self.fanouts_by_name = {} # to use within `fanout?`
9
+ self.fanout_options = {delimeter: "_"}
10
+ end
11
+
12
+ module ClassMethods
13
+ # Define fanouts for metrics as array of keys.
14
+ # Order of keys is important.
15
+ # Fanout delimeter can be set with 'delimiter' option (defaults to '_').
16
+ #
17
+ # class MyMetrics < Influxer::Metrics
18
+ # set_series "my_points"
19
+ # fanout :account_id, :user_id
20
+ # end
21
+ #
22
+ # MyMetrics.where(user_id: 1).where(account_id: 10)
23
+ # # select * from my_points_account_id_10_user_id_1
24
+ #
25
+ # class MyMetrics < Influxer::Metrics
26
+ # set_series "my_points"
27
+ # fanout :account_id, :user_id, delimiter: "."
28
+ # end
29
+ #
30
+ # MyMetrics.where(user_id: 1).where(account_id: 10).where("req_time > 1000")
31
+ # # select * from my_points.account_id.10.user_id.1 where req_time > 1000
32
+
33
+ def fanout(*args, **hargs)
34
+ self.fanout_options = self.fanout_options.merge hargs
35
+
36
+ names = args.map(&:to_s) # convert all to strings (because args can be both Symbols and Strings)
37
+
38
+ self.fanouts = (self.fanouts+names).uniq
39
+
40
+ names_hash = {}
41
+ names.each do |name|
42
+ names_hash[name] = 1
43
+ end
44
+
45
+ self.fanouts_by_name = self.fanouts_by_name.merge names_hash
46
+ end
47
+
48
+ def fanout?(key)
49
+ self.fanouts_by_name.key?(key.to_s)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,6 @@
1
+ require 'influxer/metrics/scoping'
2
+ require 'influxer/metrics/fanout'
3
+
1
4
  module Influxer
2
5
  class MetricsError < StandardError; end
3
6
  class MetricsInvalid < MetricsError; end
@@ -7,10 +10,13 @@ module Influxer
7
10
  include ActiveModel::Validations
8
11
  extend ActiveModel::Callbacks
9
12
 
13
+ include Influxer::Scoping
14
+ include Influxer::Fanout
15
+
10
16
  define_model_callbacks :write
11
17
 
12
18
  class << self
13
- delegate :select, :where, :group, :limit, :delete_all, to: :all
19
+ delegate :select, :where, :group, :merge, :time, :past, :since, :limit, :fill, :delete_all, to: :all
14
20
 
15
21
  def attributes(*attrs)
16
22
  attrs.each do |name|
@@ -32,14 +38,14 @@ module Influxer
32
38
  if args.empty?
33
39
  matches = self.to_s.match(/^(.*)Metrics$/)
34
40
  if matches.nil?
35
- @series = self.to_s.underscore
41
+ @series = self.superclass.respond_to?(:series) ? self.superclass.series : self.to_s.underscore
36
42
  else
37
43
  @series = matches[1].split("::").join("_").underscore
38
44
  end
39
45
  elsif args.first.is_a?(Proc)
40
46
  @series = args.first
41
47
  else
42
- @series = args.join(",")
48
+ @series = args
43
49
  end
44
50
  end
45
51
 
@@ -48,7 +54,11 @@ module Influxer
48
54
  end
49
55
 
50
56
  def all
51
- Relation.new self
57
+ if current_scope
58
+ current_scope.clone
59
+ else
60
+ default_scoped
61
+ end
52
62
  end
53
63
  end
54
64
 
@@ -83,13 +93,31 @@ module Influxer
83
93
  end
84
94
 
85
95
  def series
86
- self.class.series.is_a?(Proc) ? self.class.series.call(self) : self.class.series
96
+ quote_series(self.class.series)
87
97
  end
88
98
 
89
99
  def client
90
100
  Influxer.client
91
101
  end
92
102
 
103
+ def quote_series(val)
104
+ case val
105
+ when Regexp
106
+ val.inspect
107
+ when Proc
108
+ quote_series(self.class.series.call(self))
109
+ when Array
110
+ if val.length > 1
111
+ "merge(#{ val.map{ |s| quote_series(s) }.join(',') })"
112
+ else
113
+ quote_series(val.first)
114
+ end
115
+ else
116
+ '"'+val.to_s.gsub(/\"/){ %q{\"} }+'"'
117
+ end
118
+ end
119
+
93
120
  attributes :time
121
+
94
122
  end
95
123
  end
@@ -0,0 +1,33 @@
1
+ module Influxer
2
+ module FanoutQuery
3
+ # Instance methods are included to Relation
4
+ def build_fanout(key, val)
5
+ @values[:has_fanout] = true
6
+ if val.is_a?(Regexp)
7
+ @values[:fanout_rxp] = true
8
+ fanout_values[key.to_s] = val.inspect[1..-2]
9
+ else
10
+ fanout_values[key.to_s] = val.to_s
11
+ end
12
+ end
13
+
14
+ def build_series_name
15
+ if @values[:has_fanout] == true
16
+ fan_parts = [@instance.series[1..-2]]
17
+ @klass.fanouts.each do |name|
18
+ if fanout_values.key?(name)
19
+ fan_parts << name
20
+ fan_parts << fanout_values[name]
21
+ end
22
+ end
23
+ if @values[:fanout_rxp] == true
24
+ "merge(/^#{ fan_parts.join( @klass.fanout_options[:delimeter] ) }$/)"
25
+ else
26
+ @instance.quote_series(fan_parts.join(@klass.fanout_options[:delimeter]))
27
+ end
28
+ else
29
+ @instance.series
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ module Influxer
2
+ module TimeQuery
3
+ TIME_ALIASES = {
4
+ hour: '1h',
5
+ minute: '1m',
6
+ second: '1s',
7
+ ms: '1u',
8
+ week: '1w',
9
+ day: '1d',
10
+ month: '30d'
11
+ }
12
+
13
+ # Add group value to relation. To be used instead of `group("time(...)").
14
+ # Accepts symbols and strings.
15
+ #
16
+ # You can set fill value within options.
17
+ #
18
+ # Metrics.time(:hour)
19
+ # # select * from metrics group by time(1h)
20
+ #
21
+ # Metrics.time("4d", fill: 0)
22
+ # # select * from metrics group by time(4d) fill(0)
23
+
24
+ def time(val, options={})
25
+ if val.is_a?(Symbol)
26
+ @values[:time] = TIME_ALIASES[val] || '1'+val.to_s
27
+ else
28
+ @values[:time] = val
29
+ end
30
+
31
+ unless options[:fill].nil?
32
+ fill( (options[:fill] == :null) ? 'null' : options[:fill].to_i)
33
+ end
34
+ self
35
+ end
36
+
37
+ # Shortcut to define time interval with regard to current time.
38
+ # Accepts symbols and numbers.
39
+ #
40
+ # Metrics.past(:hour)
41
+ # # select * from metrics where time > now() - 1h
42
+ #
43
+ # Metrics.past(:d)
44
+ # # select * from metrics where time > now() - 1d
45
+ #
46
+ # Metrics.past(2.days)
47
+ # # select * from metrics where time > now() - 172800s
48
+
49
+ def past(val)
50
+ case val
51
+ when Symbol
52
+ where("time > now() - #{ TIME_ALIASES[val] || ('1'+val.to_s) }")
53
+ when String
54
+ where("time > now() - #{val}")
55
+ else
56
+ where("time > now() - #{val.to_i}s")
57
+ end
58
+ end
59
+
60
+ # Shortcut to define start point of the time interval.
61
+ # Accepts DateTime objects.
62
+ #
63
+ # Metrics.since(1.day.ago) # assume that now is 2014-12-31 12:00:00 UTC
64
+ # # select * from metrics where time > 1420027200s
65
+ #
66
+ # Metrics.since(Time.local(2014,12,31))
67
+ # # select * from metrics where time > 1419984000s
68
+
69
+ def since(val)
70
+ where("time > #{val.to_i}s")
71
+ end
72
+ end
73
+ end