trifle-stats 1.0.0 → 1.2.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
  SHA256:
3
- metadata.gz: 43ed8b8a05c933e5397b59bce9dc18eaee146ac62d2df67eccfbd9a53caad619
4
- data.tar.gz: 59f6d4ceba6de8977fcedf6f153fd621bd78a7055102080475c0d1f0e98f952f
3
+ metadata.gz: a42d81aa9cf531df2abc462167d9b173af444c2089f15f1d53354153b1fb18df
4
+ data.tar.gz: b5237b9c28ce0779ecdc3f129d26de062084490dd9b470b05ffb975a05ecc293
5
5
  SHA512:
6
- metadata.gz: 3a6fc973aaedd3167ea5c1b4d2f2cb7ebd8376de153f212005aae43320e59acb992d1034f8a83c5d6063e9b314f916603cd98714ecb9d0a66b02e4dd8dc8fb33
7
- data.tar.gz: adfbb9e6625f1f05b20c7f420c6a029b94086882ebd7e02fb3223449fbbc22c6b42c2ad49218b76ad8406570b1c9e2263b677ae93150fe1a6200e00dadddc8e1
6
+ metadata.gz: d835304df2db49efd76728e0de0bb1ae827680a5ec1a5d4a071a9ac60ca4a045441b8afa5628ca6e38e7f07c010c2cd550bdb15b91409e89f9211242bed17527
7
+ data.tar.gz: '08e5fa3dceb15fd8927c2320efe73ab543801ac4bac99ff80a478ea344e03b0c6f1f78c7ec23a21c5ac18d22912f4a10774f5a6389e2ae5a428402776e057fc5'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trifle-stats (1.0.0)
4
+ trifle-stats (1.2.0)
5
5
  tzinfo (~> 2.0)
6
6
 
7
7
  GEM
@@ -49,7 +49,7 @@ GEM
49
49
  rubocop-ast (1.4.1)
50
50
  parser (>= 2.7.1.5)
51
51
  ruby-progressbar (1.11.0)
52
- tzinfo (2.0.4)
52
+ tzinfo (2.0.5)
53
53
  concurrent-ruby (~> 1.0)
54
54
  unicode-display_width (1.7.0)
55
55
 
data/README.md CHANGED
@@ -12,7 +12,7 @@ Simple analytics backed by Redis, Postgres, MongoDB, Google Analytics, Segment,
12
12
 
13
13
  ## Documentation
14
14
 
15
- You can find guides and documentation at https://trifle.io/docs/stats
15
+ You can find guides and documentation at https://trifle.io/trifle-stats
16
16
 
17
17
  ## Installation
18
18
 
data/bin/console CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "trifle/stats"
5
+ require "byebug"
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -6,12 +6,13 @@ module Trifle
6
6
  module Stats
7
7
  class Configuration
8
8
  attr_writer :driver
9
- attr_accessor :track_ranges, :time_zone, :beginning_of_week
9
+ attr_accessor :track_ranges, :time_zone, :beginning_of_week, :designator
10
10
 
11
11
  def initialize
12
12
  @ranges = %i[minute hour day week month quarter year]
13
13
  @beginning_of_week = :monday
14
14
  @time_zone = 'GMT'
15
+ @designator = nil
15
16
  end
16
17
 
17
18
  def tz
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trifle
4
+ module Stats
5
+ class Designator
6
+ class Custom
7
+ attr_reader :buckets
8
+
9
+ def initialize(buckets:)
10
+ @buckets = buckets.sort
11
+ end
12
+
13
+ def designate(value:)
14
+ return buckets.first.to_s if value <= buckets.first
15
+ return "#{buckets.last}+" if value > buckets.last
16
+
17
+ (buckets.find { |b| value.ceil < b }).to_s
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trifle
4
+ module Stats
5
+ class Designator
6
+ class Geometric
7
+ attr_reader :min, :max
8
+
9
+ def initialize(min:, max:)
10
+ @min = min.negative? ? 0 : min
11
+ @max = max
12
+ end
13
+
14
+ def designate(value:) # rubocop:disable Metrics/AbcSize
15
+ return min.to_f.to_s if value <= min
16
+ return "#{max.to_f}+" if value > max
17
+ return (10**value.floor.to_s.length).to_f.to_s if value > 1
18
+ return 1.0.to_s if value > 0.1 # ugh?
19
+
20
+ (1.0 / 10**value.to_s.gsub('0.', '').split(/[1-9]/).first.length).to_s
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trifle
4
+ module Stats
5
+ class Designator
6
+ class Linear
7
+ attr_reader :min, :max, :step
8
+
9
+ def initialize(min:, max:, step:)
10
+ @min = min
11
+ @max = max
12
+ @step = step.to_i
13
+ end
14
+
15
+ def designate(value:) # rubocop:disable Metrics/AbcSize
16
+ return min.to_s if value <= min
17
+ return "#{max}+" if value > max
18
+
19
+ (value.ceil / step * step + ((value.ceil % step).zero? ? 0 : step)).to_s
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  Driver is a wrapper class that persists and retrieves values from backend. It needs to implement:
4
4
 
5
- - `inc(key:, **values)` method increment values
6
- - `set(key:, **values)` method set values
5
+ - `inc(keys:, **values)` method increment values
6
+ - `set(keys:, **values)` method set values
7
7
  - `get(keys:)` method to retrieve values
8
8
 
9
9
  ## Documentation
@@ -16,24 +16,27 @@ module Trifle
16
16
  @separator = '::'
17
17
  end
18
18
 
19
- def inc(key:, **values)
20
- pkey = key.join(separator)
19
+ def inc(keys:, **values)
20
+ data = self.class.pack(hash: { data: values })
21
21
 
22
22
  collection.bulk_write(
23
- [upsert_operation('$inc', pkey: pkey, values: values)]
23
+ keys.map do |key|
24
+ upsert_operation('$inc', pkey: key.join(separator), data: data)
25
+ end
24
26
  )
25
27
  end
26
28
 
27
- def set(key:, **values)
28
- pkey = key.join(separator)
29
+ def set(keys:, **values)
30
+ data = self.class.pack(hash: { data: values })
29
31
 
30
32
  collection.bulk_write(
31
- [upsert_operation('$set', pkey: pkey, values: values)]
33
+ keys.map do |key|
34
+ upsert_operation('$set', pkey: key.join(separator), data: data)
35
+ end
32
36
  )
33
37
  end
34
38
 
35
- def upsert_operation(operation, pkey:, values:)
36
- data = self.class.pack(hash: { data: values })
39
+ def upsert_operation(operation, pkey:, data:)
37
40
  {
38
41
  update_many: {
39
42
  filter: { key: pkey },
@@ -16,54 +16,55 @@ module Trifle
16
16
  @separator = '::'
17
17
  end
18
18
 
19
- def inc(key:, **values)
20
- pkey = key.join(separator)
19
+ def inc(keys:, **values)
20
+ keys.map do |key|
21
+ pkey = key.join(separator)
21
22
 
22
- self.class.pack(hash: values).each do |k, c|
23
- _inc_one(key: pkey, name: k, value: c)
23
+ _inc_all(key: pkey, data: self.class.pack(hash: values))
24
24
  end
25
25
  end
26
26
 
27
- def _inc_one(key:, name:, value:)
28
- data = { name => value }
29
- query = "INSERT INTO trifle_stats(key, data) VALUES ('#{key}', '#{data.to_json}') ON CONFLICT (key) DO UPDATE SET data = jsonb_set(to_jsonb(trifle_stats.data), '{#{name}}', (COALESCE(trifle_stats.data->>'#{name}','0')::int + #{value})::text::jsonb)" # rubocop:disable Metric/LineLength
27
+ def _inc_all(key:, data:)
28
+ query = "INSERT INTO trifle_stats(key, data) VALUES ('#{key}', '#{data.to_json}') ON CONFLICT (key) DO UPDATE SET data = " + # rubocop:disable Layout/LineLength
29
+ data.inject('to_jsonb(trifle_stats.data)') { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (COALESCE(trifle_stats.data->>'#{k}', '0')::int + #{v})::text::jsonb)" } # rubocop:disable Layout/LineLength
30
30
 
31
31
  client.exec(query)
32
32
  end
33
33
 
34
- def set(key:, **values)
35
- pkey = key.join(separator)
34
+ def set(keys:, **values)
35
+ keys.map do |key|
36
+ pkey = key.join(separator)
36
37
 
37
- _set_all(key: pkey, **values)
38
+ _set_all(key: pkey, data: self.class.pack(hash: values))
39
+ end
38
40
  end
39
41
 
40
- def _set_all(key:, **values)
41
- data = self.class.pack(hash: values)
42
- query = "INSERT INTO trifle_stats(key, data) VALUES ('#{key}', '#{data.to_json}') ON CONFLICT (key) DO UPDATE SET data = '#{data.to_json}'" # rubocop:disable Metric/LineLength
42
+ def _set_all(key:, data:)
43
+ query = "INSERT INTO trifle_stats(key, data) VALUES ('#{key}', '#{data.to_json}') ON CONFLICT (key) DO UPDATE SET data = " + # rubocop:disable Layout/LineLength
44
+ data.inject('to_jsonb(trifle_stats.data)') { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (#{v})::text::jsonb)" } # rubocop:disable Layout/LineLength
43
45
 
44
46
  client.exec(query)
45
47
  end
46
48
 
47
49
  def get(keys:)
48
- keys.map do |key|
49
- pkey = key.join(separator)
50
-
51
- data = _get(key: pkey)
52
- return {} if data.nil?
50
+ pkeys = keys.map { |key| key.join(separator) }
51
+ data = _get_all(keys: pkeys)
52
+ map = data.inject({}) { |o, d| o.merge(d['key'] => d['data']) }
53
53
 
54
- self.class.unpack(hash: data)
55
- end
54
+ pkeys.map { |pkey| self.class.unpack(hash: map[pkey]) || {} }
56
55
  end
57
56
 
58
- def _get(key:)
59
- result = client.exec_params(
60
- "SELECT * FROM #{table_name} WHERE key = $1 LIMIT 1;", [key]
61
- ).to_a.first
62
- return nil if result.nil?
57
+ def _get_all(keys:)
58
+ results = client.exec_params(
59
+ "SELECT * FROM #{table_name} WHERE key IN ('#{keys.join("', '")}');"
60
+ ).to_a
63
61
 
64
- JSON.parse(result.try(:fetch, 'data'))
65
- rescue JSON::ParserError
66
- nil
62
+ results.map do |r|
63
+ r['data'] = JSON.parse(r['data'])
64
+ r
65
+ rescue JSON::ParserError
66
+ r
67
+ end
67
68
  end
68
69
  end
69
70
  end
@@ -12,19 +12,23 @@ module Trifle
12
12
  @separator = '::'
13
13
  end
14
14
 
15
- def inc(key:, **values)
16
- self.class.pack(hash: values).each do |k, c|
17
- d = @data.fetch(key.join(@separator), {})
18
- d[k] = d[k].to_i + c
19
- @data[key.join(@separator)] = d
15
+ def inc(keys:, **values)
16
+ keys.map do |key|
17
+ self.class.pack(hash: values).each do |k, c|
18
+ d = @data.fetch(key.join(@separator), {})
19
+ d[k] = d[k].to_i + c
20
+ @data[key.join(@separator)] = d
21
+ end
20
22
  end
21
23
  end
22
24
 
23
- def set(key:, **values)
24
- self.class.pack(hash: values).each do |k, c|
25
- d = @data.fetch(key.join(@separator), {})
26
- d[k] = c
27
- @data[key.join(@separator)] = d
25
+ def set(keys:, **values)
26
+ keys.map do |key|
27
+ self.class.pack(hash: values).each do |k, c|
28
+ d = @data.fetch(key.join(@separator), {})
29
+ d[k] = c
30
+ @data[key.join(@separator)] = d
31
+ end
28
32
  end
29
33
  end
30
34
 
@@ -16,18 +16,22 @@ module Trifle
16
16
  @separator = '::'
17
17
  end
18
18
 
19
- def inc(key:, **values)
20
- pkey = ([prefix] + key).join(separator)
19
+ def inc(keys:, **values)
20
+ keys.map do |key|
21
+ pkey = ([prefix] + key).join(separator)
21
22
 
22
- self.class.pack(hash: values).each do |k, c|
23
- client.hincrby(pkey, k, c)
23
+ self.class.pack(hash: values).each do |k, c|
24
+ client.hincrby(pkey, k, c)
25
+ end
24
26
  end
25
27
  end
26
28
 
27
- def set(key:, **values)
28
- pkey = ([prefix] + key).join(separator)
29
+ def set(keys:, **values)
30
+ keys.map do |key|
31
+ pkey = ([prefix] + key).join(separator)
29
32
 
30
- client.hmset(pkey, *self.class.pack(hash: values))
33
+ client.hmset(pkey, *self.class.pack(hash: values))
34
+ end
31
35
  end
32
36
 
33
37
  def get(keys:)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trifle
4
+ module Stats
5
+ module Operations
6
+ module Timeseries
7
+ class Classify
8
+ attr_reader :key, :values
9
+
10
+ def initialize(**keywords)
11
+ @key = keywords.fetch(:key)
12
+ @at = keywords.fetch(:at)
13
+ @values = keywords.fetch(:values)
14
+ @config = keywords[:config]
15
+ end
16
+
17
+ def config
18
+ @config || Trifle::Stats.default
19
+ end
20
+
21
+ def deep_classify(hash)
22
+ hash.transform_values do |value|
23
+ next deep_classify(value) if value.is_a?(Hash)
24
+
25
+ { classify(value) => 1 }
26
+ end
27
+ end
28
+
29
+ def classify(value)
30
+ config.designator.designate(value: value).to_s.gsub('.', '_')
31
+ end
32
+
33
+ def key_for(range:)
34
+ at = Nocturnal.new(@at, config: config).send(range)
35
+ [key, range, at.to_i]
36
+ end
37
+
38
+ def perform
39
+ config.driver.inc(
40
+ keys: config.ranges.map { |range| key_for(range: range) },
41
+ **deep_classify(values)
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -18,14 +18,16 @@ module Trifle
18
18
  @config || Trifle::Stats.default
19
19
  end
20
20
 
21
+ def key_for(range:)
22
+ at = Nocturnal.new(@at, config: config).send(range)
23
+ [key, range, at.to_i]
24
+ end
25
+
21
26
  def perform
22
- config.ranges.map do |range|
23
- at = Nocturnal.new(@at, config: config).send(range)
24
- config.driver.inc(
25
- key: [key, range, at.to_i],
26
- **values
27
- )
28
- end
27
+ config.driver.inc(
28
+ keys: config.ranges.map { |range| key_for(range: range) },
29
+ **values
30
+ )
29
31
  end
30
32
  end
31
33
  end
@@ -18,14 +18,16 @@ module Trifle
18
18
  @config || Trifle::Stats.default
19
19
  end
20
20
 
21
+ def key_for(range:)
22
+ at = Nocturnal.new(@at, config: config).send(range)
23
+ [key, range, at.to_i]
24
+ end
25
+
21
26
  def perform
22
- config.ranges.map do |range|
23
- at = Nocturnal.new(@at, config: config).send(range)
24
- config.driver.set(
25
- key: [key, range, at.to_i],
26
- **values
27
- )
28
- end
27
+ config.driver.set(
28
+ keys: config.ranges.map { |range| key_for(range: range) },
29
+ **values
30
+ )
29
31
  end
30
32
  end
31
33
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Trifle
4
4
  module Stats
5
- VERSION = '1.0.0'
5
+ VERSION = '1.2.0'
6
6
  end
7
7
  end
data/lib/trifle/stats.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'trifle/stats/designator/custom'
4
+ require 'trifle/stats/designator/geometric'
5
+ require 'trifle/stats/designator/linear'
3
6
  require 'trifle/stats/driver/mongo'
4
7
  require 'trifle/stats/driver/postgres'
5
8
  require 'trifle/stats/driver/process'
@@ -7,6 +10,7 @@ require 'trifle/stats/driver/redis'
7
10
  require 'trifle/stats/mixins/packer'
8
11
  require 'trifle/stats/nocturnal'
9
12
  require 'trifle/stats/configuration'
13
+ require 'trifle/stats/operations/timeseries/classify'
10
14
  require 'trifle/stats/operations/timeseries/increment'
11
15
  require 'trifle/stats/operations/timeseries/set'
12
16
  require 'trifle/stats/operations/timeseries/values'
@@ -45,6 +49,15 @@ module Trifle
45
49
  ).perform
46
50
  end
47
51
 
52
+ def self.assort(key:, at:, values:, config: nil)
53
+ Trifle::Stats::Operations::Timeseries::Classify.new(
54
+ key: key,
55
+ at: at,
56
+ values: values,
57
+ config: config
58
+ ).perform
59
+ end
60
+
48
61
  def self.values(key:, from:, to:, range:, config: nil)
49
62
  Trifle::Stats::Operations::Timeseries::Values.new(
50
63
  key: key,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trifle-stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jozef Vaclavik
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-18 00:00:00.000000000 Z
11
+ date: 2022-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -183,6 +183,9 @@ files:
183
183
  - bin/setup
184
184
  - lib/trifle/stats.rb
185
185
  - lib/trifle/stats/configuration.rb
186
+ - lib/trifle/stats/designator/custom.rb
187
+ - lib/trifle/stats/designator/geometric.rb
188
+ - lib/trifle/stats/designator/linear.rb
186
189
  - lib/trifle/stats/driver/README.md
187
190
  - lib/trifle/stats/driver/mongo.rb
188
191
  - lib/trifle/stats/driver/postgres.rb
@@ -190,6 +193,7 @@ files:
190
193
  - lib/trifle/stats/driver/redis.rb
191
194
  - lib/trifle/stats/mixins/packer.rb
192
195
  - lib/trifle/stats/nocturnal.rb
196
+ - lib/trifle/stats/operations/timeseries/classify.rb
193
197
  - lib/trifle/stats/operations/timeseries/increment.rb
194
198
  - lib/trifle/stats/operations/timeseries/set.rb
195
199
  - lib/trifle/stats/operations/timeseries/values.rb