ddmetrics 1.0.0rc1

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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.verbose = false
8
+ end
9
+
10
+ task :test_samples do
11
+ sh 'bundle exec ruby samples/cache.rb > /dev/null'
12
+ end
13
+
14
+ RuboCop::RakeTask.new(:rubocop)
15
+
16
+ task default: :test
17
+
18
+ task test: %i[spec rubocop test_samples]
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/ddmetrics/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ddmetrics'
7
+ spec.version = DDMetrics::VERSION
8
+ spec.authors = ['Denis Defreyne']
9
+ spec.email = ['denis+rubygems@denis.ws']
10
+
11
+ spec.summary = 'Non-timeseries measurements for Ruby programs'
12
+ spec.homepage = 'https://github.com/ddfreyne/ddmetrics'
13
+ spec.license = 'MIT'
14
+
15
+ spec.required_ruby_version = '>= 2.3.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.require_paths = ['lib']
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ddmetrics/version'
4
+
5
+ module DDMetrics
6
+ end
7
+
8
+ require_relative 'ddmetrics/basic_counter'
9
+ require_relative 'ddmetrics/basic_summary'
10
+
11
+ require_relative 'ddmetrics/metric'
12
+ require_relative 'ddmetrics/counter'
13
+ require_relative 'ddmetrics/summary'
14
+
15
+ require_relative 'ddmetrics/stopwatch'
16
+
17
+ require_relative 'ddmetrics/table'
18
+ require_relative 'ddmetrics/printer'
19
+ require_relative 'ddmetrics/stats'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class BasicCounter
5
+ attr_reader :value
6
+
7
+ def initialize
8
+ @value = 0
9
+ end
10
+
11
+ def increment
12
+ @value += 1
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class BasicSummary
5
+ attr_reader :values
6
+
7
+ def initialize
8
+ @values = []
9
+ end
10
+
11
+ def observe(value)
12
+ @values << value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Counter < Metric
5
+ def increment(label)
6
+ validate_label(label)
7
+ basic_metric_for(label, BasicCounter).increment
8
+ end
9
+
10
+ def get(label)
11
+ validate_label(label)
12
+ basic_metric_for(label, BasicCounter).value
13
+ end
14
+
15
+ def to_s
16
+ DDMetrics::Printer.new.counter_to_s(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Metric
5
+ include Enumerable
6
+
7
+ def initialize
8
+ @basic_metrics = {}
9
+ end
10
+
11
+ def get(label)
12
+ basic_metric_for(label, BasicCounter)
13
+ end
14
+
15
+ def labels
16
+ @basic_metrics.keys
17
+ end
18
+
19
+ def each
20
+ @basic_metrics.each_key do |label|
21
+ yield(label, get(label))
22
+ end
23
+ end
24
+
25
+ # @api private
26
+ def basic_metric_for(label, basic_class)
27
+ @basic_metrics.fetch(label) { @basic_metrics[label] = basic_class.new }
28
+ end
29
+
30
+ # @api private
31
+ def validate_label(label)
32
+ return if label.is_a?(Hash)
33
+ raise ArgumentError, 'label argument must be a hash'
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Printer
5
+ def summary_to_s(summary)
6
+ DDMetrics::Table.new(table_for_summary(summary)).to_s
7
+ end
8
+
9
+ def counter_to_s(counter)
10
+ DDMetrics::Table.new(table_for_counter(counter)).to_s
11
+ end
12
+
13
+ private
14
+
15
+ def label_to_s(label)
16
+ label.to_a.sort.map { |pair| pair.join('=') }.join(' ')
17
+ end
18
+
19
+ def table_for_summary(summary)
20
+ headers = ['', 'count', 'min', '.50', '.90', '.95', 'max', 'tot']
21
+
22
+ rows = summary.labels.map do |label|
23
+ stats = summary.get(label)
24
+
25
+ count = stats.count
26
+ min = stats.min
27
+ p50 = stats.quantile(0.50)
28
+ p90 = stats.quantile(0.90)
29
+ p95 = stats.quantile(0.95)
30
+ tot = stats.sum
31
+ max = stats.max
32
+
33
+ [label_to_s(label), count.to_s] + [min, p50, p90, p95, max, tot].map { |r| format('%4.2f', r) }
34
+ end
35
+
36
+ [headers] + rows
37
+ end
38
+
39
+ def table_for_counter(counter)
40
+ headers = ['', 'count']
41
+
42
+ rows = counter.labels.map do |label|
43
+ [label_to_s(label), counter.get(label).to_s]
44
+ end
45
+
46
+ [headers] + rows
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Stats
5
+ class EmptyError < StandardError
6
+ def message
7
+ 'Not enough data to perform calculation'
8
+ end
9
+ end
10
+
11
+ def initialize(values)
12
+ @values = values
13
+ end
14
+
15
+ def inspect
16
+ "<#{self.class} count=#{count}>"
17
+ end
18
+
19
+ def count
20
+ @values.size
21
+ end
22
+
23
+ def sum
24
+ raise EmptyError if @values.empty?
25
+ @values.reduce(:+)
26
+ end
27
+
28
+ def avg
29
+ sum.to_f / count
30
+ end
31
+
32
+ def min
33
+ quantile(0.0)
34
+ end
35
+
36
+ def max
37
+ quantile(1.0)
38
+ end
39
+
40
+ def quantile(fraction)
41
+ raise EmptyError if @values.empty?
42
+
43
+ target = (@values.size - 1) * fraction.to_f
44
+ interp = target % 1.0
45
+ sorted_values[target.floor] * (1.0 - interp) + sorted_values[target.ceil] * interp
46
+ end
47
+
48
+ private
49
+
50
+ def sorted_values
51
+ @sorted_values ||= @values.sort
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Stopwatch
5
+ class AlreadyRunningError < StandardError
6
+ def message
7
+ 'Cannot start, because stopwatch is already running'
8
+ end
9
+ end
10
+
11
+ class NotRunningError < StandardError
12
+ def message
13
+ 'Cannot stop, because stopwatch is not running'
14
+ end
15
+ end
16
+
17
+ class StillRunningError < StandardError
18
+ def message
19
+ 'Cannot get duration, because stopwatch is still running'
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ @duration = 0.0
25
+ @last_start = nil
26
+ end
27
+
28
+ def start
29
+ raise AlreadyRunningError if running?
30
+ @last_start = Time.now
31
+ end
32
+
33
+ def stop
34
+ raise NotRunningError unless running?
35
+ @duration += (Time.now - @last_start)
36
+ @last_start = nil
37
+ end
38
+
39
+ def duration
40
+ raise StillRunningError if running?
41
+ @duration
42
+ end
43
+
44
+ def running?
45
+ !@last_start.nil?
46
+ end
47
+
48
+ def stopped?
49
+ !running?
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Summary < Metric
5
+ def observe(value, label)
6
+ validate_label(label)
7
+ basic_metric_for(label, BasicSummary).observe(value)
8
+ end
9
+
10
+ def get(label)
11
+ validate_label(label)
12
+ values = basic_metric_for(label, BasicSummary).values
13
+ DDMetrics::Stats.new(values)
14
+ end
15
+
16
+ def to_s
17
+ DDMetrics::Printer.new.summary_to_s(self)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ class Table
5
+ def initialize(rows)
6
+ @rows = rows
7
+ end
8
+
9
+ def to_s
10
+ columns = @rows.transpose
11
+ column_lengths = columns.map { |c| c.map(&:size).max }
12
+
13
+ [].tap do |lines|
14
+ lines << row_to_s(@rows[0], column_lengths)
15
+ lines << separator(column_lengths)
16
+ rows = sort_rows(@rows.drop(1))
17
+ lines.concat(rows.map { |r| row_to_s(r, column_lengths) })
18
+ end.join("\n")
19
+ end
20
+
21
+ private
22
+
23
+ def sort_rows(rows)
24
+ rows.sort_by { |r| r.first.downcase }
25
+ end
26
+
27
+ def row_to_s(row, column_lengths)
28
+ values = row.zip(column_lengths).map { |text, length| text.rjust(length) }
29
+ values[0] + ' │ ' + values[1..-1].join(' ')
30
+ end
31
+
32
+ def separator(column_lengths)
33
+ (+'').tap do |s|
34
+ s << '─' * column_lengths[0]
35
+ s << '─┼─'
36
+ s << column_lengths[1..-1].map { |l| '─' * l }.join('───')
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDMetrics
4
+ VERSION = '1.0.0rc1'
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ddmetrics'
4
+
5
+ class Cache
6
+ attr_reader :counter
7
+
8
+ def initialize
9
+ @map = {}
10
+ @counter = DDMetrics::Counter.new
11
+ end
12
+
13
+ def []=(key, value)
14
+ @counter.increment(type: :set)
15
+
16
+ @map[key] = value
17
+ end
18
+
19
+ def [](key)
20
+ if @map.key?(key)
21
+ @counter.increment(type: :get_hit)
22
+ else
23
+ @counter.increment(type: :get_miss)
24
+ end
25
+
26
+ @map[key]
27
+ end
28
+ end
29
+
30
+ cache = Cache.new
31
+
32
+ cache['greeting']
33
+ cache['greeting']
34
+ cache['greeting'] = 'Hi there!'
35
+ cache['greeting']
36
+ cache['greeting']
37
+ cache['greeting']
38
+
39
+ p cache.counter.get(type: :set)
40
+ # => 1
41
+
42
+ p cache.counter.get(type: :get_hit)
43
+ # => 3
44
+
45
+ p cache.counter.get(type: :get_miss)
46
+ # => 2
47
+
48
+ puts cache.counter
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'netrc'
7
+ require 'octokit'
8
+ require 'shellwords'
9
+ require 'uri'
10
+
11
+ def run(*args)
12
+ puts('<exec> ' + args.map { |s| Shellwords.escape(s) }.join(' '))
13
+ system(*args)
14
+ end
15
+
16
+ gem_name = 'ddmetrics'
17
+ version_constant = 'DDMetrics::VERSION'
18
+ gem_path = 'ddmetrics'
19
+
20
+ puts '=== Logging in to GitHub’s API…'
21
+ client = Octokit::Client.new(netrc: true)
22
+ puts
23
+
24
+ puts '=== Deleting old *.gem files…'
25
+ Dir['*.gem'].each do |fn|
26
+ puts "deleting #{fn}…"
27
+ FileUtils.rm_f(fn)
28
+ end
29
+ puts
30
+
31
+ puts '=== Verifying presence of release date…'
32
+ unless File.readlines('NEWS.md').drop(2).first =~ / \(\d{4}-\d{2}-\d{2}\)$/
33
+ warn 'No proper release date found!'
34
+ exit 1
35
+ end
36
+ puts
37
+
38
+ puts '=== Reading version…'
39
+ require "./lib/#{gem_path}/version"
40
+ version = eval(version_constant) # rubocop:disable Security/Eval
41
+ puts "Version = #{version}"
42
+ puts
43
+
44
+ puts '=== Building new gem…'
45
+ run('gem', 'build', 'ddmetrics.gemspec')
46
+ puts
47
+
48
+ puts '=== Verifying that gems were built properly…'
49
+ gem_filename = "#{gem_name}-#{version}.gem"
50
+ unless File.file?(gem_filename)
51
+ warn "Error: Could not find gem: #{gem_filename}"
52
+ exit 1
53
+ end
54
+ puts
55
+
56
+ puts '=== Verifying that gem version does not yet exist…'
57
+ url = URI.parse("https://rubygems.org/api/v1/versions/#{gem_name}.json")
58
+ response = Net::HTTP.get_response(url)
59
+ existing_versions =
60
+ case response.code
61
+ when '404'
62
+ []
63
+ when '200'
64
+ JSON.parse(response.body).map { |e| e.fetch('number') }
65
+ else
66
+ warn "Error: Couldn’t fetch version information for #{gem_name} (status #{response.code})"
67
+ exit 1
68
+ end
69
+ if existing_versions.include?(version)
70
+ warn "Error: #{gem_name} v#{version} already exists"
71
+ exit 1
72
+ end
73
+ puts
74
+
75
+ puts '=== Verifying that release does not yet exist…'
76
+ releases = client.releases('ddfreyne/ddmetrics')
77
+ release = releases.find { |r| r.tag_name == DDMetrics::VERSION }
78
+ if release
79
+ warn 'Release already exists!'
80
+ warn 'ABORTED!'
81
+ exit 1
82
+ end
83
+ puts
84
+
85
+ puts '=== Creating Git tag…'
86
+ run('git', 'tag', '--sign', '--annotate', DDMetrics::VERSION, '--message', "Version #{DDMetrics::VERSION}")
87
+ puts
88
+
89
+ puts '=== Pushing Git data…'
90
+ run('git', 'push', 'origin', '--tags')
91
+ puts
92
+
93
+ puts '=== Pushing gem…'
94
+ run('gem', 'push', "ddmetrics-#{DDMetrics::VERSION}.gem")
95
+ puts
96
+
97
+ puts '=== Reading release notes…'
98
+ release_notes =
99
+ File.readlines('NEWS.md')
100
+ .drop(4)
101
+ .take_while { |l| l !~ /^## / }
102
+ .join
103
+ puts
104
+
105
+ puts '=== Creating release on GitHub…'
106
+ sleep 3 # Give GitHub some time to detect the new tag
107
+ is_prerelease = DDMetrics::VERSION =~ /a|b|rc/ || DDMetrics::VERSION =~ /^0/
108
+ client.create_release(
109
+ 'ddfreyne/ddmetrics', DDMetrics::VERSION,
110
+ prerelease: !is_prerelease.nil?,
111
+ body: release_notes
112
+ )
113
+ puts
114
+
115
+ puts 'DONE!'