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