trifle-stats 0.4.0 → 1.1.1

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.
@@ -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
@@ -9,29 +9,28 @@ name: Ruby
9
9
 
10
10
  on:
11
11
  push:
12
- branches: [ main ]
12
+ branches: [main]
13
13
  pull_request:
14
- branches: [ main ]
14
+ branches: [main]
15
15
 
16
16
  jobs:
17
17
  test:
18
-
19
18
  runs-on: ubuntu-latest
20
19
  strategy:
21
20
  matrix:
22
- ruby-version: ['2.6', '2.7', '3.0']
21
+ ruby-version: ["3.0"]
23
22
 
24
23
  steps:
25
- - uses: actions/checkout@v2
26
- - name: Set up Ruby
27
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
- # uses: ruby/setup-ruby@v1
30
- uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
31
- with:
32
- ruby-version: ${{ matrix.ruby-version }}
33
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
- - name: Rspec
35
- run: bundle exec rspec
36
- - name: Rubocop
37
- run: bundle exec rubocop
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby
26
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
27
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
28
+ # uses: ruby/setup-ruby@v1
29
+ uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
30
+ with:
31
+ ruby-version: ${{ matrix.ruby-version }}
32
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
33
+ - name: Rspec
34
+ run: bundle exec rspec
35
+ - name: Rubocop
36
+ run: bundle exec rubocop
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.0)
5
- mongo (>= 2.14.0)
6
- pg (>= 1.2)
7
- redis (>= 4.2)
4
+ trifle-stats (1.1.1)
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)
@@ -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:)
@@ -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