trifle-stats 1.0.0 → 1.2.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
  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