ddtelemetry 1.0.0a1 → 1.0.0a2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/NEWS.md +6 -0
- data/README.md +143 -20
- data/Rakefile +5 -1
- data/lib/ddtelemetry.rb +6 -7
- data/lib/ddtelemetry/basic_counter.rb +15 -0
- data/lib/ddtelemetry/basic_summary.rb +15 -0
- data/lib/ddtelemetry/counter.rb +8 -6
- data/lib/ddtelemetry/metric.rb +30 -0
- data/lib/ddtelemetry/printer.rb +45 -0
- data/lib/ddtelemetry/stats.rb +54 -0
- data/lib/ddtelemetry/stopwatch.rb +11 -2
- data/lib/ddtelemetry/summary.rb +8 -45
- data/lib/ddtelemetry/version.rb +1 -1
- data/roadmap.md +7 -0
- data/samples/cache.rb +48 -0
- data/spec/ddtelemetry/basic_counter_spec.rb +20 -0
- data/spec/ddtelemetry/basic_summary_spec.rb +23 -0
- data/spec/ddtelemetry/counter_spec.rb +86 -5
- data/spec/ddtelemetry/stats_spec.rb +87 -0
- data/spec/ddtelemetry/stopwatch_spec.rb +19 -4
- data/spec/ddtelemetry/summary_spec.rb +51 -45
- data/spec/ddtelemetry_spec.rb +0 -25
- data/spec/spec_helper.rb +1 -1
- metadata +12 -7
- data/lib/ddtelemetry/labelled_counter.rb +0 -35
- data/lib/ddtelemetry/labelled_summary.rb +0 -37
- data/lib/ddtelemetry/registry.rb +0 -18
- data/spec/ddtelemetry/labelled_counter_spec.rb +0 -94
- data/spec/ddtelemetry/labelled_summary_spec.rb +0 -78
@@ -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
|
data/lib/ddtelemetry/summary.rb
CHANGED
@@ -1,55 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DDTelemetry
|
4
|
-
class Summary
|
5
|
-
|
6
|
-
|
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
|
12
|
-
|
9
|
+
def get(label)
|
10
|
+
values = basic_metric_for(label, BasicSummary).values
|
11
|
+
DDTelemetry::Stats.new(values)
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
|
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
|
data/lib/ddtelemetry/version.rb
CHANGED
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
|
-
|
7
|
-
|
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.
|
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 }
|
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 }
|
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
|