trifle-stats 0.4.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
2
+ # Initialization code that may require console input (password prompts, [y/n]
3
+ # confirmations, etc.) must go above this block; everything else may go below.
4
+ if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
5
+ source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
6
+ fi
7
+
8
+ source /root/powerlevel10k/powerlevel10k.zsh-theme
9
+
10
+ # To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
11
+ [[ ! -f /root/.p10k.zsh ]] || source /root/.p10k.zsh
12
+
13
+ POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true
@@ -0,0 +1,54 @@
1
+ FROM debian:latest
2
+
3
+ ENV TZ=Europe/Bratislava
4
+ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
5
+
6
+ # install ubuntu packages
7
+ RUN apt-get update -q \
8
+ && apt-get install -y \
9
+ build-essential \
10
+ apt-transport-https \
11
+ zsh \
12
+ libpq-dev \
13
+ git \
14
+ curl \
15
+ wget \
16
+ unzip \
17
+ gpg \
18
+ gnupg2 \
19
+ locales \
20
+ autoconf \
21
+ libssl-dev \
22
+ libreadline-dev \
23
+ zlib1g-dev \
24
+ tmux \
25
+ htop \
26
+ vim \
27
+ && apt-get clean
28
+
29
+ #set the locale
30
+ RUN locale-gen en_US.UTF-8
31
+ ENV LANG en_US.UTF-8
32
+ ENV LANGUAGE en_US:en
33
+ ENV LC_ALL en_US.UTF-8
34
+
35
+ RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true
36
+ RUN git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /root/powerlevel10k
37
+ COPY .zshrc /root/.zshrc
38
+ COPY .p10k.zsh /root/p10k.zsh
39
+
40
+ #install asdf
41
+ ENV ASDF_ROOT /root/.asdf
42
+ ENV PATH "${ASDF_ROOT}/bin:${ASDF_ROOT}/shims:$PATH"
43
+ RUN git clone https://github.com/asdf-vm/asdf.git ${ASDF_ROOT} --branch v0.10.0
44
+
45
+ RUN asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git
46
+
47
+ # install ruby
48
+ ENV RUBY_VERSION 3.0.0
49
+ RUN ASDF_RUBY_BUILD_VERSION=v20201225 asdf install ruby ${RUBY_VERSION} \
50
+ && asdf global ruby ${RUBY_VERSION}
51
+
52
+ RUN gem install bundler
53
+
54
+ CMD ["zsh"]
File without changes
File without changes
data/.gitpod.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  image:
2
- file: .gitpod/Dockerfile
2
+ file: .devops/docker/gitpod/Dockerfile
3
3
  tasks:
4
4
  - init: bundle install
5
5
  command: ./bin/console
data/Gemfile.lock CHANGED
@@ -1,10 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trifle-stats (0.4.1)
5
- mongo (>= 2.14.0)
6
- pg (>= 1.2)
7
- redis (>= 4.2)
4
+ trifle-stats (1.1.2)
8
5
  tzinfo (~> 2.0)
9
6
 
10
7
  GEM
@@ -13,7 +10,7 @@ GEM
13
10
  ast (2.4.2)
14
11
  bson (4.12.1)
15
12
  byebug (11.1.3)
16
- concurrent-ruby (1.1.9)
13
+ concurrent-ruby (1.1.10)
17
14
  diff-lcs (1.4.4)
18
15
  dotenv (2.7.6)
19
16
  mongo (2.14.0)
@@ -52,7 +49,7 @@ GEM
52
49
  rubocop-ast (1.4.1)
53
50
  parser (>= 2.7.1.5)
54
51
  ruby-progressbar (1.11.0)
55
- tzinfo (2.0.4)
52
+ tzinfo (2.0.5)
56
53
  concurrent-ruby (~> 1.0)
57
54
  unicode-display_width (1.7.0)
58
55
 
@@ -64,7 +61,10 @@ DEPENDENCIES
64
61
  bundler (~> 2.1)
65
62
  byebug
66
63
  dotenv
64
+ mongo (>= 2.14.0)
65
+ pg (>= 1.2)
67
66
  rake (~> 12.0)
67
+ redis (>= 4.2)
68
68
  rspec (~> 3.0)
69
69
  rubocop (= 1.0.0)
70
70
  trifle-stats!
data/README.md CHANGED
@@ -8,7 +8,7 @@ Simple analytics backed by Redis, Postgres, MongoDB, Google Analytics, Segment,
8
8
 
9
9
  `Trifle::Stats` is a _way too_ simple timeline analytics that helps you track custom metrics. Automatically increments counters for each enabled range. It supports timezones and different week beginning.
10
10
 
11
- [^1]: TBH only Redis for now 💔.
11
+ [^1]: TBH only Redis, Postgres and MongoDB for now 💔.
12
12
 
13
13
  ## Documentation
14
14
 
@@ -24,11 +24,22 @@ gem 'trifle-stats'
24
24
 
25
25
  And then execute:
26
26
 
27
- $ bundle install
27
+ ```sh
28
+ $ bundle install
29
+ ```
28
30
 
29
31
  Or install it yourself as:
30
32
 
31
- $ gem install trifle-stats
33
+ ```sh
34
+ $ gem install trifle-stats
35
+ ```
36
+
37
+ Depending on driver you would like to use, make sure you add required gems into your `Gemfile`.
38
+ ```ruby
39
+ gem 'mongo', '>= 2.14.0'
40
+ gem 'pg', '>= 1.2'
41
+ gem 'redis', '>= 4.2'
42
+ ```
32
43
 
33
44
  ## Usage
34
45
 
@@ -45,25 +56,72 @@ end
45
56
 
46
57
  ### Track values
47
58
 
48
- Available ranges are `:minute`, `:hour`, `:day`, `:week`, `:month`, `:quarter`, `:year`.
59
+ Track your first metrics
49
60
 
50
- Now track your first metrics
51
61
  ```ruby
52
62
  Trifle::Stats.track(key: 'event::logs', at: Time.now, values: {count: 1, duration: 2, lines: 241})
53
63
  => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>2, :lines=>241}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>2, :lines=>241}}]
54
- # or do it few more times
64
+ ```
65
+
66
+ Then do it few more times
67
+
68
+ ```ruby
55
69
  Trifle::Stats.track(key: 'event::logs', at: Time.now, values: {count: 1, duration: 1, lines: 56})
56
70
  => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>1, :lines=>56}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>1, :lines=>56}}]
57
71
  Trifle::Stats.track(key: 'event::logs', at: Time.now, values: {count: 1, duration: 5, lines: 361})
58
72
  => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>5, :lines=>361}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>5, :lines=>361}}]
59
73
  ```
60
74
 
61
- ### Get values
75
+ You can also store nested counters like
76
+
77
+ ```ruby
78
+ Trifle::Stats.track(key: 'event::logs', at: Time.now, values: {
79
+ count: 1,
80
+ duration: {
81
+ parsing: 21,
82
+ compression: 8,
83
+ upload: 1
84
+ },
85
+ lines: 25432754
86
+ })
87
+ ```
88
+
89
+ #### Get values
90
+
91
+ Retrieve your values for specific `range`. Adding increments above will return sum of all the values you've tracked.
92
+
93
+ ```ruby
94
+ Trifle::Stats.values(key: 'event::logs', from: Time.now, to: Time.now, range: :day)
95
+ => {:at=>[2021-01-25 00:00:00 +0200], :values=>[{"count"=>3, "duration"=>8, "lines"=>658}]}
96
+ ```
97
+
98
+ ### Assert values
99
+
100
+ Asserting values works same way like incrementing, but instead of increment, it sets the value. Duh.
101
+
102
+ Set your first metrics
103
+
104
+ ```ruby
105
+ Trifle::Stats.assert(key: 'event::logs', at: Time.now, values: {count: 1, duration: 2, lines: 241})
106
+ => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>2, :lines=>241}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>2, :lines=>241}}]
107
+ ```
108
+
109
+ Then do it few more times
110
+
111
+ ```ruby
112
+ Trifle::Stats.assert(key: 'event::logs', at: Time.now, values: {count: 1, duration: 1, lines: 56})
113
+ => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>1, :lines=>56}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>1, :lines=>56}}]
114
+ Trifle::Stats.assert(key: 'event::logs', at: Time.now, values: {count: 1, duration: 5, lines: 361})
115
+ => [{2021-01-25 16:00:00 +0100=>{:count=>1, :duration=>5, :lines=>361}}, {2021-01-25 00:00:00 +0100=>{:count=>1, :duration=>5, :lines=>361}}]
116
+ ```
117
+
118
+ #### Get values
119
+
120
+ Retrieve your values for specific `range`. As you just used `assert` above, it will return latest value you've asserted.
62
121
 
63
- Retrieve your values for specific `range`.
64
122
  ```ruby
65
123
  Trifle::Stats.values(key: 'event::logs', from: Time.now, to: Time.now, range: :day)
66
- => [{2021-01-25 00:00:00 +0100=>{"count"=>3, "duration"=>8, "lines"=>658}}]
124
+ => {:at=>[2021-01-25 00:00:00 +0200], :values=>[{"count"=>1, "duration"=>5, "lines"=>361}]}
67
125
  ```
68
126
 
69
127
  ## Contributing
@@ -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,9 +2,9 @@
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
7
- - `get(key:)` method to retrieve values
5
+ - `inc(keys:, **values)` method increment values
6
+ - `set(keys:, **values)` method set values
7
+ - `get(keys:)` method to retrieve values
8
8
 
9
9
  ## Documentation
10
10
 
@@ -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 },
@@ -43,13 +46,12 @@ module Trifle
43
46
  }
44
47
  end
45
48
 
46
- def get(key:)
47
- pkey = key.join(separator)
48
-
49
- data = collection.find(key: pkey).limit(1).first
50
- return {} if data.nil? || data['data'].nil?
49
+ def get(keys:)
50
+ pkeys = keys.map { |key| key.join(separator) }
51
+ data = collection.find(key: { '$in' => pkeys })
52
+ map = data.inject({}) { |o, d| o.merge(d['key'] => d['data']) }
51
53
 
52
- data['data']
54
+ pkeys.map { |pkey| map[pkey] || {} }
53
55
  end
54
56
 
55
57
  private
@@ -16,11 +16,13 @@ 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
+ self.class.pack(hash: values).each do |k, c|
24
+ _inc_one(key: pkey, name: k, value: c)
25
+ end
24
26
  end
25
27
  end
26
28
 
@@ -31,10 +33,12 @@ module Trifle
31
33
  client.exec(query)
32
34
  end
33
35
 
34
- def set(key:, **values)
35
- pkey = key.join(separator)
36
+ def set(keys:, **values)
37
+ keys.map do |key|
38
+ pkey = key.join(separator)
36
39
 
37
- _set_all(key: pkey, **values)
40
+ _set_all(key: pkey, **values)
41
+ end
38
42
  end
39
43
 
40
44
  def _set_all(key:, **values)
@@ -44,13 +48,15 @@ module Trifle
44
48
  client.exec(query)
45
49
  end
46
50
 
47
- def get(key:)
48
- pkey = key.join(separator)
51
+ def get(keys:)
52
+ keys.map do |key|
53
+ pkey = key.join(separator)
49
54
 
50
- data = _get(key: pkey)
51
- return {} if data.nil?
55
+ data = _get(key: pkey)
56
+ return {} if data.nil?
52
57
 
53
- self.class.unpack(hash: data)
58
+ self.class.unpack(hash: data)
59
+ end
54
60
  end
55
61
 
56
62
  def _get(key:)
@@ -59,7 +65,7 @@ module Trifle
59
65
  ).to_a.first
60
66
  return nil if result.nil?
61
67
 
62
- JSON.parse(result.try(:fetch, 'data'))
68
+ JSON.parse(result['data'])
63
69
  rescue JSON::ParserError
64
70
  nil
65
71
  end
@@ -12,26 +12,32 @@ 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
 
31
- def get(key:)
32
- self.class.unpack(
33
- hash: @data.fetch(key.join(@separator), {})
34
- )
35
+ def get(keys:)
36
+ keys.map do |key|
37
+ self.class.unpack(
38
+ hash: @data.fetch(key.join(@separator), {})
39
+ )
40
+ end
35
41
  end
36
42
  end
37
43
  end
@@ -16,26 +16,32 @@ 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
- def get(key:)
34
- pkey = ([prefix] + key).join(separator)
37
+ def get(keys:)
38
+ keys.map do |key|
39
+ pkey = ([prefix] + key).join(separator)
35
40
 
36
- self.class.unpack(
37
- hash: client.hgetall(pkey)
38
- )
41
+ self.class.unpack(
42
+ hash: client.hgetall(pkey)
43
+ )
44
+ end
39
45
  end
40
46
  end
41
47
  end
@@ -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
@@ -24,13 +24,14 @@ module Trifle
24
24
  end
25
25
 
26
26
  def perform
27
- timeline.map do |at|
28
- {
29
- at => config.driver.get(
30
- key: [key, range, at.to_i]
31
- )
32
- }
33
- end
27
+ {
28
+ at: timeline,
29
+ values: config.driver.get(
30
+ keys: timeline.map do |at|
31
+ [key, range, at.to_i]
32
+ end
33
+ )
34
+ }
34
35
  end
35
36
  end
36
37
  end