ddmetrics 1.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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!'