ddtelemetry 1.0.0a1 → 1.0.0a2

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.
@@ -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