ddtelemetry 1.0.0a1 → 1.0.0a2

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,8 +2,6 @@
2
2
 
3
3
  module DDTelemetry
4
4
  class Stopwatch
5
- attr_reader :duration
6
-
7
5
  class AlreadyRunningError < StandardError
8
6
  def message
9
7
  'Cannot start, because stopwatch is already running'
@@ -16,6 +14,12 @@ module DDTelemetry
16
14
  end
17
15
  end
18
16
 
17
+ class StillRunningError < StandardError
18
+ def message
19
+ 'Cannot get duration, because stopwatch is still running'
20
+ end
21
+ end
22
+
19
23
  def initialize
20
24
  @duration = 0.0
21
25
  @last_start = nil
@@ -32,6 +36,11 @@ module DDTelemetry
32
36
  @last_start = nil
33
37
  end
34
38
 
39
+ def duration
40
+ raise StillRunningError if running?
41
+ @duration
42
+ end
43
+
35
44
  def running?
36
45
  !@last_start.nil?
37
46
  end
@@ -1,55 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DDTelemetry
4
- class Summary
5
- class EmptySummaryError < StandardError
6
- def message
7
- 'Cannot calculate quantile for empty summary'
8
- end
4
+ class Summary < Metric
5
+ def observe(value, label)
6
+ basic_metric_for(label, BasicSummary).observe(value)
9
7
  end
10
8
 
11
- def initialize
12
- @values = []
9
+ def get(label)
10
+ values = basic_metric_for(label, BasicSummary).values
11
+ DDTelemetry::Stats.new(values)
13
12
  end
14
13
 
15
- def observe(value)
16
- @values << value
17
- @sorted_values = nil
18
- end
19
-
20
- def count
21
- @values.size
22
- end
23
-
24
- def sum
25
- raise EmptySummaryError if @values.empty?
26
- @values.reduce(:+)
27
- end
28
-
29
- def avg
30
- sum / count
31
- end
32
-
33
- def min
34
- quantile(0.0)
35
- end
36
-
37
- def max
38
- quantile(1.0)
39
- end
40
-
41
- def quantile(fraction)
42
- raise EmptySummaryError if @values.empty?
43
-
44
- target = (@values.size - 1) * fraction.to_f
45
- interp = target % 1.0
46
- sorted_values[target.floor] * (1.0 - interp) + sorted_values[target.ceil] * interp
47
- end
48
-
49
- private
50
-
51
- def sorted_values
52
- @sorted_values ||= @values.sort
14
+ def to_s
15
+ DDTelemetry::Printer.new.summary_to_s(self)
53
16
  end
54
17
  end
55
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DDTelemetry
4
- VERSION = '1.0.0a1'
4
+ VERSION = '1.0.0a2'
5
5
  end
data/roadmap.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Roadmap
2
+
3
+ This document details the changes need to get to a stable 1.0.
4
+
5
+ * **Don’t use the term “telemetry”:** The term “telemetry” specifically refers to collecting data from remote systems. DDTelemetry does not at all collect from remote systems (in fact, that is explicitly out-of-scope). Suggestion: DDMetrics?
6
+
7
+ * **Key-value labels:** Labels can currently be any object. Stabilising on key-value labels, with keys being symbols and labels being strings, would make it easier to create tooling around transforming and analysing datasets that are produced by DDTelemetry.
data/samples/cache.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ddtelemetry'
4
+
5
+ class Cache
6
+ attr_reader :counter
7
+
8
+ def initialize
9
+ @map = {}
10
+ @counter = DDTelemetry::Counter.new
11
+ end
12
+
13
+ def []=(key, value)
14
+ @counter.increment(:set)
15
+
16
+ @map[key] = value
17
+ end
18
+
19
+ def [](key)
20
+ if @map.key?(key)
21
+ @counter.increment(:get_hit)
22
+ else
23
+ @counter.increment(: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(:set)
40
+ # => 1
41
+
42
+ p cache.counter.get(:get_hit)
43
+ # => 3
44
+
45
+ p cache.counter.get(:get_miss)
46
+ # => 2
47
+
48
+ puts cache.counter
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::BasicCounter do
4
+ subject(:counter) { described_class.new }
5
+
6
+ it 'starts at 0' do
7
+ expect(counter.value).to eq(0)
8
+ end
9
+
10
+ describe '#increment' do
11
+ subject { counter.increment }
12
+
13
+ it 'increments' do
14
+ expect { subject }
15
+ .to change { counter.value }
16
+ .from(0)
17
+ .to(1)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::BasicSummary do
4
+ subject(:summary) { described_class.new }
5
+
6
+ context 'no observations' do
7
+ its(:values) { is_expected.to be_empty }
8
+ end
9
+
10
+ context 'one observation' do
11
+ before { subject.observe(2.1) }
12
+ its(:values) { is_expected.to eq([2.1]) }
13
+ end
14
+
15
+ context 'two observations' do
16
+ before do
17
+ subject.observe(2.1)
18
+ subject.observe(4.1)
19
+ end
20
+
21
+ its(:values) { is_expected.to eq([2.1, 4.1]) }
22
+ end
23
+ end
@@ -3,18 +3,99 @@
3
3
  describe DDTelemetry::Counter do
4
4
  subject(:counter) { described_class.new }
5
5
 
6
- it 'starts at 0' do
7
- expect(counter.value).to eq(0)
6
+ describe 'new counter' do
7
+ it 'starts at 0' do
8
+ expect(subject.get(:erb)).to eq(0)
9
+ expect(subject.get(:haml)).to eq(0)
10
+ end
8
11
  end
9
12
 
10
13
  describe '#increment' do
11
- subject { counter.increment }
14
+ subject { counter.increment(:erb) }
12
15
 
13
- it 'increments' do
16
+ it 'increments the matching value' do
14
17
  expect { subject }
15
- .to change { counter.value }
18
+ .to change { counter.get(:erb) }
16
19
  .from(0)
17
20
  .to(1)
18
21
  end
22
+
23
+ it 'does not increment any other value' do
24
+ expect(counter.get(:haml)).to eq(0)
25
+ expect { subject }
26
+ .not_to change { counter.get(:haml) }
27
+ end
28
+ end
29
+
30
+ describe '#get' do
31
+ subject { counter.get(:erb) }
32
+
33
+ context 'not incremented' do
34
+ it { is_expected.to eq(0) }
35
+ end
36
+
37
+ context 'incremented' do
38
+ before { counter.increment(:erb) }
39
+ it { is_expected.to eq(1) }
40
+ end
41
+
42
+ context 'other incremented' do
43
+ before { counter.increment(:haml) }
44
+ it { is_expected.to eq(0) }
45
+ end
46
+ end
47
+
48
+ describe '#labels' do
49
+ subject { counter.labels }
50
+
51
+ before do
52
+ counter.increment(:erb)
53
+ counter.increment(:erb)
54
+ counter.increment(:haml)
55
+ end
56
+
57
+ it { is_expected.to contain_exactly(:haml, :erb) }
58
+ end
59
+
60
+ describe '#each' do
61
+ subject do
62
+ {}.tap do |res|
63
+ counter.each { |label, count| res[label] = count }
64
+ end
65
+ end
66
+
67
+ before do
68
+ counter.increment(:erb)
69
+ counter.increment(:erb)
70
+ counter.increment(:haml)
71
+ end
72
+
73
+ it { is_expected.to eq(haml: 1, erb: 2) }
74
+
75
+ it 'is enumerable' do
76
+ expect(counter.map { |_label, count| count }.sort)
77
+ .to eq([1, 2])
78
+ end
79
+ end
80
+
81
+ describe '#to_s' do
82
+ subject { counter.to_s }
83
+
84
+ before do
85
+ counter.increment(:erb)
86
+ counter.increment(:erb)
87
+ counter.increment(:haml)
88
+ end
89
+
90
+ it 'returns table' do
91
+ expected = <<~TABLE
92
+ │ count
93
+ ─────┼──────
94
+ erb │ 2
95
+ haml │ 1
96
+ TABLE
97
+
98
+ expect(subject.strip).to eq(expected.strip)
99
+ end
19
100
  end
20
101
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::Stats do
4
+ subject(:stats) { described_class.new(values) }
5
+
6
+ context 'no values' do
7
+ let(:values) { [] }
8
+
9
+ it 'errors on #min' do
10
+ expect { subject.min }
11
+ .to raise_error(DDTelemetry::Stats::EmptyError)
12
+ end
13
+
14
+ it 'errors on #max' do
15
+ expect { subject.max }
16
+ .to raise_error(DDTelemetry::Stats::EmptyError)
17
+ end
18
+
19
+ it 'errors on #avg' do
20
+ expect { subject.avg }
21
+ .to raise_error(DDTelemetry::Stats::EmptyError)
22
+ end
23
+
24
+ it 'errors on #sum' do
25
+ expect { subject.sum }
26
+ .to raise_error(DDTelemetry::Stats::EmptyError)
27
+ end
28
+
29
+ its(:count) { is_expected.to eq(0) }
30
+ end
31
+
32
+ context 'one value' do
33
+ let(:values) { [2.1] }
34
+
35
+ its(:inspect) { is_expected.to eq('<DDTelemetry::Stats count=1>') }
36
+ its(:count) { is_expected.to eq(1) }
37
+ its(:sum) { is_expected.to eq(2.1) }
38
+ its(:avg) { is_expected.to eq(2.1) }
39
+ its(:min) { is_expected.to eq(2.1) }
40
+ its(:max) { is_expected.to eq(2.1) }
41
+
42
+ it 'has proper quantiles' do
43
+ expect(subject.quantile(0.00)).to eq(2.1)
44
+ expect(subject.quantile(0.25)).to eq(2.1)
45
+ expect(subject.quantile(0.50)).to eq(2.1)
46
+ expect(subject.quantile(0.90)).to eq(2.1)
47
+ expect(subject.quantile(0.99)).to eq(2.1)
48
+ end
49
+ end
50
+
51
+ context 'two values' do
52
+ let(:values) { [2.1, 4.1] }
53
+
54
+ its(:inspect) { is_expected.to eq('<DDTelemetry::Stats count=2>') }
55
+ its(:count) { is_expected.to be_within(0.000001).of(2) }
56
+ its(:sum) { is_expected.to be_within(0.000001).of(6.2) }
57
+ its(:avg) { is_expected.to be_within(0.000001).of(3.1) }
58
+ its(:min) { is_expected.to be_within(0.000001).of(2.1) }
59
+ its(:max) { is_expected.to be_within(0.000001).of(4.1) }
60
+
61
+ it 'has proper quantiles' do
62
+ expect(subject.quantile(0.00)).to be_within(0.000001).of(2.1)
63
+ expect(subject.quantile(0.25)).to be_within(0.000001).of(2.6)
64
+ expect(subject.quantile(0.50)).to be_within(0.000001).of(3.1)
65
+ expect(subject.quantile(0.90)).to be_within(0.000001).of(3.9)
66
+ expect(subject.quantile(0.99)).to be_within(0.000001).of(4.08)
67
+ end
68
+ end
69
+
70
+ context 'integer values' do
71
+ let(:values) { [1, 2] }
72
+
73
+ its(:count) { is_expected.to be_within(0.000001).of(2) }
74
+ its(:sum) { is_expected.to be_within(0.000001).of(3) }
75
+ its(:avg) { is_expected.to be_within(0.000001).of(1.5) }
76
+ its(:min) { is_expected.to be_within(0.000001).of(1) }
77
+ its(:max) { is_expected.to be_within(0.000001).of(2) }
78
+
79
+ it 'has proper quantiles' do
80
+ expect(subject.quantile(0.00)).to be_within(0.000001).of(1.0)
81
+ expect(subject.quantile(0.25)).to be_within(0.000001).of(1.25)
82
+ expect(subject.quantile(0.50)).to be_within(0.000001).of(1.5)
83
+ expect(subject.quantile(0.90)).to be_within(0.000001).of(1.9)
84
+ expect(subject.quantile(0.99)).to be_within(0.000001).of(1.99)
85
+ end
86
+ end
87
+ end
@@ -9,8 +9,6 @@ describe DDTelemetry::Stopwatch do
9
9
  expect(stopwatch.duration).to eq(0.0)
10
10
  end
11
11
 
12
- # TODO: if running, raise error when asking for #duration
13
-
14
12
  it 'records correct duration after start+stop' do
15
13
  Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
16
14
  stopwatch.start
@@ -38,12 +36,20 @@ describe DDTelemetry::Stopwatch do
38
36
  end
39
37
 
40
38
  it 'errors when stopping when not started' do
41
- expect { stopwatch.stop }.to raise_error(DDTelemetry::Stopwatch::NotRunningError)
39
+ expect { stopwatch.stop }
40
+ .to raise_error(
41
+ DDTelemetry::Stopwatch::NotRunningError,
42
+ 'Cannot stop, because stopwatch is not running',
43
+ )
42
44
  end
43
45
 
44
46
  it 'errors when starting when already started' do
45
47
  stopwatch.start
46
- expect { stopwatch.start }.to raise_error(DDTelemetry::Stopwatch::AlreadyRunningError)
48
+ expect { stopwatch.start }
49
+ .to raise_error(
50
+ DDTelemetry::Stopwatch::AlreadyRunningError,
51
+ 'Cannot start, because stopwatch is already running',
52
+ )
47
53
  end
48
54
 
49
55
  it 'reports running status' do
@@ -60,4 +66,13 @@ describe DDTelemetry::Stopwatch do
60
66
  expect(stopwatch).not_to be_running
61
67
  expect(stopwatch).to be_stopped
62
68
  end
69
+
70
+ it 'errors when getting duration while running' do
71
+ stopwatch.start
72
+ expect { stopwatch.duration }
73
+ .to raise_error(
74
+ DDTelemetry::Stopwatch::StillRunningError,
75
+ 'Cannot get duration, because stopwatch is still running',
76
+ )
77
+ end
63
78
  end