ddtelemetry 1.0.0a1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 410462bde4ad03647c35d4eb9706a513e8453e74d2218e9d60cc2f19b0a8f935
4
+ data.tar.gz: 8392f095624dc46eb70c463fdbf9e004fad56c273670eb54e650ae044140d101
5
+ SHA512:
6
+ metadata.gz: 8fc60025054933c5f5b07a0d72bb3c457c67016324ce9670d6eee63b952f8ab5cdc0fd4cd85bdc92dc5e82b0f44eff69272137c7b1087b5aba9e6321afeca423
7
+ data.tar.gz: 34e45164626c7ec0b40382ac3c1fe4fd020a412c12b8903a93c34fac557210ef87045d69664de4a8198798d4be2d93433535ee3243da2613723efc28cf422663
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ *.gem
3
+ /.vscode/
4
+ /coverage/
5
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ -r ./spec/spec_helper.rb
2
+ --format Fuubar
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,59 @@
1
+ # ----- CONFIGURED -----
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.3
5
+ DisplayCopNames: true
6
+
7
+ Style/TrailingCommaInArguments:
8
+ EnforcedStyleForMultiline: comma
9
+
10
+ Style/TrailingCommaInLiteral:
11
+ EnforcedStyleForMultiline: comma
12
+
13
+ Layout/IndentArray:
14
+ EnforcedStyle: consistent
15
+
16
+ # Documentation lives elsewhere, and not everything needs to be documented.
17
+ Style/Documentation:
18
+ Enabled: false
19
+
20
+ # This doesn’t work well with RSpec.
21
+ Lint/AmbiguousBlockAssociation:
22
+ Exclude:
23
+ - spec/**/*_spec.rb
24
+
25
+
26
+
27
+ # ----- DISABLED (metrics) -----
28
+
29
+ # Cops for metrics are disabled because they should not cause tests to fail.
30
+
31
+ Metrics/AbcSize:
32
+ Enabled: false
33
+
34
+ Metrics/BlockLength:
35
+ Enabled: false
36
+
37
+ Metrics/BlockNesting:
38
+ Enabled: false
39
+
40
+ Metrics/ClassLength:
41
+ Enabled: false
42
+
43
+ Metrics/CyclomaticComplexity:
44
+ Enabled: false
45
+
46
+ Metrics/LineLength:
47
+ Enabled: false
48
+
49
+ Metrics/MethodLength:
50
+ Enabled: false
51
+
52
+ Metrics/ModuleLength:
53
+ Enabled: false
54
+
55
+ Metrics/ParameterLists:
56
+ Enabled: false
57
+
58
+ Metrics/PerceivedComplexity:
59
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - "2.3"
5
+ - "2.4"
6
+
7
+ branches:
8
+ only:
9
+ - "master"
10
+
11
+ cache: bundler
12
+
13
+ sudo: false
14
+
15
+ git:
16
+ depth: 10
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at denis.defreyne@stoneship.org. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :devel do
8
+ gem 'codecov', require: false
9
+ gem 'fuubar'
10
+ gem 'rake'
11
+ gem 'rspec'
12
+ gem 'rspec-its'
13
+ gem 'rubocop', '~> 0.50'
14
+ gem 'timecop', '~> 0.9'
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Denis Defreyne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/NEWS.md ADDED
@@ -0,0 +1,5 @@
1
+ # DDTelemetry news
2
+
3
+ ## 1.0.0a1 (2017-12-02)
4
+
5
+ Initial release.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ [![Gem version](https://img.shields.io/gem/v/ddtelemetry.svg)](http://rubygems.org/gems/ddtelemetry)
2
+ [![Gem downloads](https://img.shields.io/gem/dt/ddtelemetry.svg)](http://rubygems.org/gems/ddtelemetry)
3
+ [![Build status](https://img.shields.io/travis/ddfreyne/ddtelemetry.svg)](https://travis-ci.org/ddfreyne/ddtelemetry)
4
+ [![Code Climate](https://img.shields.io/codeclimate/github/ddfreyne/ddtelemetry.svg)](https://codeclimate.com/github/ddfreyne/ddtelemetry)
5
+ [![Code Coverage](https://img.shields.io/codecov/c/github/ddfreyne/ddtelemetry.svg)](https://codecov.io/gh/ddfreyne/ddtelemetry)
6
+
7
+ # DDTelemetry
8
+
9
+ _DDTelemetry_ provides in-process, non-timeseries telemetry for short-running Ruby processes.
10
+
11
+ ⚠️ This project is **experimental** and should not be used in production yet.
12
+
13
+ If you are looking for a full-featured timeseries monitoring system, look no further than [Prometheus](https://prometheus.io/).
14
+
15
+ ## Example
16
+
17
+ Take the following (naïve) cache implementation as a starting point:
18
+
19
+ ```ruby
20
+ class Cache
21
+ def initialize
22
+ @map = {}
23
+ end
24
+
25
+ def []=(key, value)
26
+ @map[key] = value
27
+ end
28
+
29
+ def [](key)
30
+ @map[key]
31
+ end
32
+ end
33
+ ```
34
+
35
+ To start instrumenting this code, require `ddtelemetry`, pass a telemetry instance into the constructor, and record some metrics:
36
+
37
+ ```ruby
38
+ require 'ddtelemetry'
39
+
40
+ class Cache
41
+ def initialize(telemetry)
42
+ @telemetry = telemetry
43
+ @map = {}
44
+ end
45
+
46
+ def []=(key, value)
47
+ @telemetry.counter(:cache).increment(:set)
48
+
49
+ @map[key] = value
50
+ end
51
+
52
+ def [](key)
53
+ if @map.key?(key)
54
+ @telemetry.counter(:cache).increment([:get, :hit])
55
+ else
56
+ @telemetry.counter(:cache).increment([:get, :miss])
57
+ end
58
+
59
+ @map[key]
60
+ end
61
+ end
62
+ ```
63
+
64
+ Let’s construct a cache (with telemetry) and exercise it:
65
+
66
+ ```ruby
67
+ telemetry = DDTelemetry::Registry.new
68
+ cache = Cache.new(telemetry)
69
+
70
+ cache['greeting']
71
+ cache['greeting']
72
+ cache['greeting'] = 'Hi there!'
73
+ cache['greeting']
74
+ cache['greeting']
75
+ cache['greeting']
76
+ ```
77
+
78
+ Finally, print the recorded telemetry values:
79
+
80
+ ```ruby
81
+ p telemetry.counter(:cache).value(:set)
82
+ # => 1
83
+
84
+ p telemetry.counter(:cache).value([:get, :hit])
85
+ # => 3
86
+
87
+ p telemetry.counter(:cache).value([:get, :miss])
88
+ # => 2
89
+ ```
90
+
91
+ ## Installation
92
+
93
+ Add this line to your application's Gemfile:
94
+
95
+ ```ruby
96
+ gem 'ddtelemetry'
97
+ ```
98
+
99
+ And then execute:
100
+
101
+ $ bundle
102
+
103
+ Or install it yourself as:
104
+
105
+ $ gem install ddtelemetry
106
+
107
+ ## Usage
108
+
109
+ TODO
110
+
111
+ ## Development
112
+
113
+ Install dependencies:
114
+
115
+ $ bundle
116
+
117
+ Run tests:
118
+
119
+ $ bundle exec rake
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ddfreyne/ddtelemetry. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
124
+
125
+ ## License
126
+
127
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
128
+
129
+ ## Code of Conduct
130
+
131
+ Everyone interacting in the DDTelemetry project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ddfreyne/ddtelemetry/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
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
+ RuboCop::RakeTask.new(:rubocop)
11
+
12
+ task default: :test
13
+
14
+ task test: %i[spec rubocop]
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/ddtelemetry/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ddtelemetry'
7
+ spec.version = DDTelemetry::VERSION
8
+ spec.authors = ['Denis Defreyne']
9
+ spec.email = ['denis+rubygems@denis.ws']
10
+
11
+ spec.summary = 'Non-timeseries telemetry'
12
+ spec.homepage = 'https://github.com/ddfreyne/ddtelemetry'
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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class Counter
5
+ attr_reader :value
6
+
7
+ def initialize
8
+ @value = 0
9
+ end
10
+
11
+ def increment
12
+ @value += 1
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class LabelledCounter
5
+ def initialize
6
+ @counters = {}
7
+ end
8
+
9
+ def increment(label)
10
+ get(label).increment
11
+ end
12
+
13
+ def get(label)
14
+ @counters.fetch(label) { @counters[label] = Counter.new }
15
+ end
16
+
17
+ def empty?
18
+ @counters.empty?
19
+ end
20
+
21
+ def value(label)
22
+ get(label).value
23
+ end
24
+
25
+ def values
26
+ @counters.each_with_object({}) do |(label, counter), res|
27
+ res[label] = counter.value
28
+ end
29
+ end
30
+
31
+ def map
32
+ @counters.map { |(label, counter)| yield(label, counter) }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class LabelledSummary
5
+ def initialize
6
+ @summaries = {}
7
+ end
8
+
9
+ def observe(value, label)
10
+ get(label).observe(value)
11
+ end
12
+
13
+ def get(label)
14
+ @summaries.fetch(label) { @summaries[label] = Summary.new }
15
+ end
16
+
17
+ def empty?
18
+ @summaries.empty?
19
+ end
20
+
21
+ def quantile(fraction, label)
22
+ get(label).quantile(fraction)
23
+ end
24
+
25
+ def map
26
+ @summaries.map { |(label, summary)| yield(label, summary) }
27
+ end
28
+
29
+ # TODO: add quantiles(fraction)
30
+ # TODO: add min(label)
31
+ # TODO: add mins
32
+ # TODO: add max(label)
33
+ # TODO: add maxs
34
+ # TODO: add sum(label)
35
+ # TODO: add sums
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class Registry
5
+ def initialize
6
+ @counters = {}
7
+ @summaries = {}
8
+ end
9
+
10
+ def counter(name)
11
+ @counters.fetch(name) { @counters[name] = LabelledCounter.new }
12
+ end
13
+
14
+ def summary(name)
15
+ @summaries.fetch(name) { @summaries[name] = LabelledSummary.new }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class Stopwatch
5
+ attr_reader :duration
6
+
7
+ class AlreadyRunningError < StandardError
8
+ def message
9
+ 'Cannot start, because stopwatch is already running'
10
+ end
11
+ end
12
+
13
+ class NotRunningError < StandardError
14
+ def message
15
+ 'Cannot stop, because stopwatch is not running'
16
+ end
17
+ end
18
+
19
+ def initialize
20
+ @duration = 0.0
21
+ @last_start = nil
22
+ end
23
+
24
+ def start
25
+ raise AlreadyRunningError if running?
26
+ @last_start = Time.now
27
+ end
28
+
29
+ def stop
30
+ raise NotRunningError unless running?
31
+ @duration += (Time.now - @last_start)
32
+ @last_start = nil
33
+ end
34
+
35
+ def running?
36
+ !@last_start.nil?
37
+ end
38
+
39
+ def stopped?
40
+ !running?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ class Summary
5
+ class EmptySummaryError < StandardError
6
+ def message
7
+ 'Cannot calculate quantile for empty summary'
8
+ end
9
+ end
10
+
11
+ def initialize
12
+ @values = []
13
+ end
14
+
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
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
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
+ lines.concat(@rows.drop(1).map { |r| row_to_s(r, column_lengths) })
17
+ end.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ def row_to_s(row, column_lengths)
23
+ values = row.zip(column_lengths).map { |text, length| text.rjust(length) }
24
+ values[0] + ' │ ' + values[1..-1].join(' ')
25
+ end
26
+
27
+ def separator(column_lengths)
28
+ (+'').tap do |s|
29
+ s << '─' * column_lengths[0]
30
+ s << '─┼─'
31
+ s << column_lengths[1..-1].map { |l| '─' * l }.join('───')
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DDTelemetry
4
+ VERSION = '1.0.0a1'
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ddtelemetry/version'
4
+
5
+ module DDTelemetry
6
+ def self.new
7
+ Registry.new
8
+ end
9
+ end
10
+
11
+ require_relative 'ddtelemetry/counter'
12
+ require_relative 'ddtelemetry/summary'
13
+
14
+ require_relative 'ddtelemetry/labelled_counter'
15
+ require_relative 'ddtelemetry/labelled_summary'
16
+
17
+ require_relative 'ddtelemetry/registry'
18
+ require_relative 'ddtelemetry/stopwatch'
19
+
20
+ require_relative 'ddtelemetry/table'
data/scripts/release ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'octokit'
6
+
7
+ def run(*args)
8
+ puts 'I will execute the following:'
9
+ puts ' ' + args.map { |a| a =~ /\s/ ? a.inspect : a }.join(' ')
10
+ print 'Is this correct? [y/N] '
11
+ res = gets
12
+ unless res.strip.casecmp('y').zero?
13
+ warn 'Answer was not Y; release aborted.'
14
+ exit 1
15
+ end
16
+
17
+ system('echo', *args)
18
+ system(*args)
19
+
20
+ print 'Continue? [y/N] '
21
+ res = gets
22
+ unless res.strip.casecmp('y').zero?
23
+ warn 'Answer was not Y; release aborted.'
24
+ exit 1
25
+ end
26
+
27
+ true
28
+ end
29
+
30
+ puts '=== Logging in to GitHub’s API…'
31
+ client = Octokit::Client.new(netrc: true)
32
+ puts
33
+
34
+ puts '=== Deleting old *.gem files…'
35
+ Dir['*.gem'].each do |fn|
36
+ puts " #{fn}…"
37
+ FileUtils.rm_f(fn)
38
+ end
39
+ puts
40
+
41
+ puts '=== Verifying presence of release date…'
42
+ unless File.readlines('NEWS.md').drop(2).first =~ / \(\d{4}-\d{2}-\d{2}\)$/
43
+ warn 'No proper release date found!'
44
+ exit 1
45
+ end
46
+ puts
47
+
48
+ puts '=== Building new gem…'
49
+ run('gem', 'build', 'ddtelemetry.gemspec')
50
+ puts
51
+
52
+ puts '=== Reading version…'
53
+ require './lib/ddtelemetry/version'
54
+ puts "Version = #{DDTelemetry::VERSION}"
55
+ puts
56
+
57
+ puts '=== Verifying that release does not yet exist…'
58
+ releases = client.releases('ddfreyne/ddtelemetry')
59
+ release = releases.find { |r| r.tag_name == DDTelemetry::VERSION }
60
+ if release
61
+ warn 'Release already exists!'
62
+ warn 'ABORTED!'
63
+ exit 1
64
+ end
65
+ puts
66
+
67
+ puts '=== Creating Git tag…'
68
+ run('git', 'tag', '--sign', '--annotate', DDTelemetry::VERSION, '--message', "Version #{DDTelemetry::VERSION}")
69
+ puts
70
+
71
+ puts '=== Pushing Git data…'
72
+ run('git', 'push', 'origin', '--tags')
73
+ puts
74
+
75
+ puts '=== Pushing gem…'
76
+ run('gem', 'push', "ddtelemetry-#{DDTelemetry::VERSION}.gem")
77
+ puts
78
+
79
+ puts '=== Reading release notes…'
80
+ release_notes =
81
+ File.readlines('NEWS.md')
82
+ .drop(4)
83
+ .take_while { |l| l !~ /^## / }
84
+ .join
85
+ puts
86
+
87
+ puts '=== Creating release on GitHub…'
88
+ sleep 3 # Give GitHub some time to detect the new tag
89
+ is_prerelease = DDTelemetry::VERSION =~ /a|b|rc/ || DDTelemetry::VERSION =~ /^0/
90
+ client.create_release(
91
+ 'ddfreyne/ddtelemetry', DDTelemetry::VERSION,
92
+ prerelease: !is_prerelease.nil?,
93
+ body: release_notes
94
+ )
95
+ puts
96
+
97
+ puts 'DONE!'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::Counter 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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::LabelledCounter do
4
+ subject(:counter) { described_class.new }
5
+
6
+ describe 'new counter' do
7
+ it 'starts at 0' do
8
+ expect(subject.value(:erb)).to eq(0)
9
+ expect(subject.value(:haml)).to eq(0)
10
+ end
11
+
12
+ it 'has no values' do
13
+ expect(subject.values).to eq({})
14
+ end
15
+ end
16
+
17
+ describe '#increment' do
18
+ subject { counter.increment(:erb) }
19
+
20
+ it 'increments the matching value' do
21
+ expect { subject }
22
+ .to change { counter.value(:erb) }
23
+ .from(0)
24
+ .to(1)
25
+ end
26
+
27
+ it 'does not increment any other value' do
28
+ expect(counter.value(:haml)).to eq(0)
29
+ expect { subject }
30
+ .not_to change { counter.value(:haml) }
31
+ end
32
+
33
+ it 'correctly changes #values' do
34
+ expect { subject }
35
+ .to change { counter.values }
36
+ .from({})
37
+ .to(erb: 1)
38
+ end
39
+ end
40
+
41
+ describe '#get' do
42
+ subject { counter.get(:erb) }
43
+
44
+ context 'not incremented' do
45
+ its(:value) { is_expected.to eq(0) }
46
+ end
47
+
48
+ context 'incremented' do
49
+ before { counter.increment(:erb) }
50
+ its(:value) { is_expected.to eq(1) }
51
+ end
52
+
53
+ context 'other incremented' do
54
+ before { counter.increment(:haml) }
55
+ its(:value) { is_expected.to eq(0) }
56
+ end
57
+ end
58
+
59
+ describe '#empty?' do
60
+ subject { counter.empty? }
61
+
62
+ context 'not incremented' do
63
+ it { is_expected.to be }
64
+ end
65
+
66
+ context 'incremented' do
67
+ before { counter.increment(:erb) }
68
+ it { is_expected.not_to be }
69
+ end
70
+ end
71
+
72
+ describe '#map' do
73
+ subject { counter.map { |label, counter| [label, counter.value] } }
74
+
75
+ context 'not incremented' do
76
+ it { is_expected.to be_empty }
77
+ end
78
+
79
+ context 'incremented once' do
80
+ before { counter.increment(:erb) }
81
+ it { is_expected.to eq [[:erb, 1]] }
82
+ end
83
+
84
+ context 'both incremental multiple times' do
85
+ before do
86
+ counter.increment(:erb)
87
+ counter.increment(:erb)
88
+ counter.increment(:haml)
89
+ end
90
+
91
+ it { is_expected.to eq [[:erb, 2], [:haml, 1]] }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::LabelledSummary do
4
+ subject(:summary) { described_class.new }
5
+
6
+ describe '#empty?' do
7
+ subject { summary.empty? }
8
+
9
+ context 'empty summary' do
10
+ it { is_expected.to be }
11
+ end
12
+
13
+ context 'some observations' do
14
+ before do
15
+ summary.observe(7.2, :erb)
16
+ summary.observe(5.3, :erb)
17
+ summary.observe(3.0, :haml)
18
+ end
19
+
20
+ it { is_expected.not_to be }
21
+ end
22
+ end
23
+
24
+ describe '#get' do
25
+ subject { summary.get(:erb) }
26
+
27
+ context 'empty summary' do
28
+ its(:count) { is_expected.to eq(0) }
29
+ end
30
+
31
+ context 'one observation with that label' do
32
+ before { summary.observe(0.1, :erb) }
33
+ its(:count) { is_expected.to eq(1) }
34
+ end
35
+
36
+ context 'one observation with a different label' do
37
+ before { summary.observe(0.1, :haml) }
38
+ its(:count) { is_expected.to eq(0) }
39
+ end
40
+ end
41
+
42
+ describe '#map' do
43
+ before do
44
+ subject.observe(2.1, :erb)
45
+ subject.observe(4.1, :erb)
46
+ subject.observe(5.3, :haml)
47
+ end
48
+
49
+ it 'yields label and summary' do
50
+ res = subject.map { |label, summary| [label, summary.avg.round(3)] }
51
+ expect(res).to eql([[:erb, 3.1], [:haml, 5.3]])
52
+ end
53
+ end
54
+
55
+ describe '#quantile' do
56
+ before do
57
+ subject.observe(2.1, :erb)
58
+ subject.observe(4.1, :erb)
59
+ subject.observe(5.3, :haml)
60
+ end
61
+
62
+ it 'has proper quantiles for :erb' do
63
+ expect(subject.quantile(0.00, :erb)).to be_within(0.000001).of(2.1)
64
+ expect(subject.quantile(0.25, :erb)).to be_within(0.000001).of(2.6)
65
+ expect(subject.quantile(0.50, :erb)).to be_within(0.000001).of(3.1)
66
+ expect(subject.quantile(0.90, :erb)).to be_within(0.000001).of(3.9)
67
+ expect(subject.quantile(0.99, :erb)).to be_within(0.000001).of(4.08)
68
+ end
69
+
70
+ it 'has proper quantiles for :erb' do
71
+ expect(subject.quantile(0.00, :haml)).to be_within(0.000001).of(5.3)
72
+ expect(subject.quantile(0.25, :haml)).to be_within(0.000001).of(5.3)
73
+ expect(subject.quantile(0.50, :haml)).to be_within(0.000001).of(5.3)
74
+ expect(subject.quantile(0.90, :haml)).to be_within(0.000001).of(5.3)
75
+ expect(subject.quantile(0.99, :haml)).to be_within(0.000001).of(5.3)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::Stopwatch do
4
+ subject(:stopwatch) { described_class.new }
5
+
6
+ after { Timecop.return }
7
+
8
+ it 'is zero by default' do
9
+ expect(stopwatch.duration).to eq(0.0)
10
+ end
11
+
12
+ # TODO: if running, raise error when asking for #duration
13
+
14
+ it 'records correct duration after start+stop' do
15
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
16
+ stopwatch.start
17
+
18
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
19
+ stopwatch.stop
20
+
21
+ expect(stopwatch.duration).to eq(1.0)
22
+ end
23
+
24
+ it 'records correct duration after start+stop+start+stop' do
25
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 0))
26
+ stopwatch.start
27
+
28
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 1))
29
+ stopwatch.stop
30
+
31
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 3))
32
+ stopwatch.start
33
+
34
+ Timecop.freeze(Time.local(2008, 9, 1, 10, 5, 6))
35
+ stopwatch.stop
36
+
37
+ expect(stopwatch.duration).to eq(1.0 + 3.0)
38
+ end
39
+
40
+ it 'errors when stopping when not started' do
41
+ expect { stopwatch.stop }.to raise_error(DDTelemetry::Stopwatch::NotRunningError)
42
+ end
43
+
44
+ it 'errors when starting when already started' do
45
+ stopwatch.start
46
+ expect { stopwatch.start }.to raise_error(DDTelemetry::Stopwatch::AlreadyRunningError)
47
+ end
48
+
49
+ it 'reports running status' do
50
+ expect(stopwatch).not_to be_running
51
+ expect(stopwatch).to be_stopped
52
+
53
+ stopwatch.start
54
+
55
+ expect(stopwatch).to be_running
56
+ expect(stopwatch).not_to be_stopped
57
+
58
+ stopwatch.stop
59
+
60
+ expect(stopwatch).not_to be_running
61
+ expect(stopwatch).to be_stopped
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::Summary do
4
+ subject(:summary) { described_class.new }
5
+
6
+ context 'no observations' do
7
+ it 'errors on #min' do
8
+ expect { subject.min }
9
+ .to raise_error(DDTelemetry::Summary::EmptySummaryError)
10
+ end
11
+
12
+ it 'errors on #max' do
13
+ expect { subject.max }
14
+ .to raise_error(DDTelemetry::Summary::EmptySummaryError)
15
+ end
16
+
17
+ it 'errors on #avg' do
18
+ expect { subject.avg }
19
+ .to raise_error(DDTelemetry::Summary::EmptySummaryError)
20
+ end
21
+
22
+ it 'errors on #sum' do
23
+ expect { subject.sum }
24
+ .to raise_error(DDTelemetry::Summary::EmptySummaryError)
25
+ end
26
+
27
+ its(:count) { is_expected.to eq(0) }
28
+ end
29
+
30
+ context 'one observation' do
31
+ before { subject.observe(2.1) }
32
+
33
+ its(:count) { is_expected.to eq(1) }
34
+ its(:sum) { is_expected.to eq(2.1) }
35
+ its(:avg) { is_expected.to eq(2.1) }
36
+ its(:min) { is_expected.to eq(2.1) }
37
+ its(:max) { is_expected.to eq(2.1) }
38
+
39
+ it 'has proper quantiles' do
40
+ expect(subject.quantile(0.00)).to eq(2.1)
41
+ expect(subject.quantile(0.25)).to eq(2.1)
42
+ expect(subject.quantile(0.50)).to eq(2.1)
43
+ expect(subject.quantile(0.90)).to eq(2.1)
44
+ expect(subject.quantile(0.99)).to eq(2.1)
45
+ end
46
+ end
47
+
48
+ context 'two observations' do
49
+ before do
50
+ subject.observe(2.1)
51
+ subject.observe(4.1)
52
+ end
53
+
54
+ its(:count) { is_expected.to be_within(0.000001).of(2) }
55
+ its(:sum) { is_expected.to be_within(0.000001).of(6.2) }
56
+ its(:avg) { is_expected.to be_within(0.000001).of(3.1) }
57
+ its(:min) { is_expected.to be_within(0.000001).of(2.1) }
58
+ its(:max) { is_expected.to be_within(0.000001).of(4.1) }
59
+
60
+ it 'has proper quantiles' do
61
+ expect(subject.quantile(0.00)).to be_within(0.000001).of(2.1)
62
+ expect(subject.quantile(0.25)).to be_within(0.000001).of(2.6)
63
+ expect(subject.quantile(0.50)).to be_within(0.000001).of(3.1)
64
+ expect(subject.quantile(0.90)).to be_within(0.000001).of(3.9)
65
+ expect(subject.quantile(0.99)).to be_within(0.000001).of(4.08)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry::Table do
4
+ let(:table) { described_class.new(rows) }
5
+
6
+ let(:rows) do
7
+ [
8
+ %w[name awesomeness],
9
+ %w[denis high],
10
+ %w[REDACTED low],
11
+ ]
12
+ end
13
+
14
+ example do
15
+ expect(table.to_s).to eq(<<~TABLE.rstrip)
16
+ name │ awesomeness
17
+ ─────────┼────────────
18
+ denis │ high
19
+ REDACTED │ low
20
+ TABLE
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe DDTelemetry do
4
+ it 'has a version number' do
5
+ expect(DDTelemetry::VERSION).not_to be nil
6
+ end
7
+
8
+ subject { described_class.new }
9
+
10
+ example do
11
+ expect(subject.counter(:filters).values).to eq({})
12
+ expect(subject.counter(:filters).get(:erb).value).to eq(0)
13
+ expect(subject.counter(:filters).value(:erb)).to eq(0)
14
+
15
+ subject.counter(:filters).increment(:erb)
16
+ expect(subject.counter(:filters).values).to eq(erb: 1)
17
+ expect(subject.counter(:filters).get(:erb).value).to eq(1)
18
+ expect(subject.counter(:filters).value(:erb)).to eq(1)
19
+ end
20
+
21
+ example do
22
+ subject.summary(:filters).observe(0.1, :erb)
23
+ expect(subject.summary(:filters).quantile(0.0, :erb)).to be_within(0.00001).of(0.1)
24
+ expect(subject.summary(:filters).quantile(0.5, :erb)).to be_within(0.00001).of(0.1)
25
+ expect(subject.summary(:filters).quantile(1.0, :erb)).to be_within(0.00001).of(0.1)
26
+
27
+ subject.summary(:filters).observe(1.1, :erb)
28
+ expect(subject.summary(:filters).quantile(0.0, :erb)).to be_within(0.00001).of(0.1)
29
+ expect(subject.summary(:filters).quantile(0.5, :erb)).to be_within(0.00001).of(0.6)
30
+ expect(subject.summary(:filters).quantile(1.0, :erb)).to be_within(0.00001).of(1.1)
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'codecov'
7
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
8
+
9
+ require 'ddtelemetry'
10
+
11
+ require 'fuubar'
12
+ require 'rspec/its'
13
+ require 'timecop'
14
+
15
+ RSpec.configure do |c|
16
+ c.fuubar_progress_bar_options = {
17
+ format: '%c/%C |<%b>%i| %p%%',
18
+ }
19
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ddtelemetry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0a1
5
+ platform: ruby
6
+ authors:
7
+ - Denis Defreyne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - denis+rubygems@denis.ws
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - ".travis.yml"
24
+ - CODE_OF_CONDUCT.md
25
+ - Gemfile
26
+ - LICENSE.txt
27
+ - NEWS.md
28
+ - README.md
29
+ - Rakefile
30
+ - ddtelemetry.gemspec
31
+ - lib/ddtelemetry.rb
32
+ - lib/ddtelemetry/counter.rb
33
+ - lib/ddtelemetry/labelled_counter.rb
34
+ - lib/ddtelemetry/labelled_summary.rb
35
+ - lib/ddtelemetry/registry.rb
36
+ - lib/ddtelemetry/stopwatch.rb
37
+ - lib/ddtelemetry/summary.rb
38
+ - lib/ddtelemetry/table.rb
39
+ - lib/ddtelemetry/version.rb
40
+ - scripts/release
41
+ - spec/ddtelemetry/counter_spec.rb
42
+ - spec/ddtelemetry/labelled_counter_spec.rb
43
+ - spec/ddtelemetry/labelled_summary_spec.rb
44
+ - spec/ddtelemetry/stopwatch_spec.rb
45
+ - spec/ddtelemetry/summary_spec.rb
46
+ - spec/ddtelemetry/table_spec.rb
47
+ - spec/ddtelemetry_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/ddfreyne/ddtelemetry
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.3.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">"
65
+ - !ruby/object:Gem::Version
66
+ version: 1.3.1
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.7.3
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Non-timeseries telemetry
73
+ test_files: []