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