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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rubocop.yml +59 -0
- data/.travis.yml +21 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/NEWS.md +23 -0
- data/README.md +260 -0
- data/Rakefile +18 -0
- data/ddmetrics.gemspec +19 -0
- data/lib/ddmetrics.rb +19 -0
- data/lib/ddmetrics/basic_counter.rb +15 -0
- data/lib/ddmetrics/basic_summary.rb +15 -0
- data/lib/ddmetrics/counter.rb +19 -0
- data/lib/ddmetrics/metric.rb +36 -0
- data/lib/ddmetrics/printer.rb +49 -0
- data/lib/ddmetrics/stats.rb +54 -0
- data/lib/ddmetrics/stopwatch.rb +52 -0
- data/lib/ddmetrics/summary.rb +20 -0
- data/lib/ddmetrics/table.rb +40 -0
- data/lib/ddmetrics/version.rb +5 -0
- data/samples/cache.rb +48 -0
- data/scripts/release +115 -0
- data/spec/ddmetrics/basic_counter_spec.rb +20 -0
- data/spec/ddmetrics/basic_summary_spec.rb +23 -0
- data/spec/ddmetrics/counter_spec.rb +109 -0
- data/spec/ddmetrics/stats_spec.rb +87 -0
- data/spec/ddmetrics/stopwatch_spec.rb +78 -0
- data/spec/ddmetrics/summary_spec.rb +84 -0
- data/spec/ddmetrics/table_spec.rb +47 -0
- data/spec/ddmetrics_spec.rb +7 -0
- data/spec/spec_helper.rb +19 -0
- metadata +77 -0
data/Rakefile
ADDED
@@ -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]
|
data/ddmetrics.gemspec
ADDED
@@ -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
|
data/lib/ddmetrics.rb
ADDED
@@ -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,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
|
data/samples/cache.rb
ADDED
@@ -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
|
data/scripts/release
ADDED
@@ -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!'
|