prometheus-client-tracer 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11dbb55ad9c5ce0135dd4376dff72e95b1f6e5ae378e1e4c6de2ce23a4ddc915
4
+ data.tar.gz: 762b4e6b78aaa60c82c97470a9cf1df136b0e3fe685396df117beee3671c7012
5
+ SHA512:
6
+ metadata.gz: 6291fbf2dcb56141208005a94df27e22531cd3e7c012551a809ab3287602702d91d8a499a3cea580b3b2b8ef07c677898abdb7c5f0a06296b923652a94e60b66
7
+ data.tar.gz: ec047172d19516f20a24996adf32ceabb651e03627923e30b2e1ef90e096107f0f123e263cb2ff6325a5cbabfc86a4044a681046c54ebcfcd440c726a76817ea
@@ -0,0 +1,42 @@
1
+ ---
2
+ version: 2
3
+
4
+ references:
5
+ steps: &steps
6
+ - checkout
7
+
8
+ - type: cache-restore
9
+ key: prometheus-client-tracer-bundler-{{ checksum "prometheus-client-tracer.gemspec" }}
10
+
11
+ - run: gem install bundler -v 2.0.1
12
+ - run: bundle install --path vendor/bundle
13
+
14
+ - type: cache-save
15
+ key: prometheus-client-tracer-bundler-{{ checksum "prometheus-client-tracer.gemspec" }}
16
+ paths:
17
+ - vendor/bundle
18
+
19
+ - run: bundle exec rubocop
20
+ - run: bundle exec rspec
21
+
22
+ jobs:
23
+ build-ruby24:
24
+ docker:
25
+ - image: ruby:2.4
26
+ steps: *steps
27
+ build-ruby25:
28
+ docker:
29
+ - image: ruby:2.5
30
+ steps: *steps
31
+ build-ruby26:
32
+ docker:
33
+ - image: ruby:2.6
34
+ steps: *steps
35
+
36
+ workflows:
37
+ version: 2
38
+ tests:
39
+ jobs:
40
+ - build-ruby24
41
+ - build-ruby25
42
+ - build-ruby26
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require ./spec/spec_helper.rb --color
@@ -0,0 +1,25 @@
1
+ ---
2
+ inherit_gem:
3
+ gc_ruboconfig: rubocop.yml
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.4
7
+
8
+ Metrics/MethodLength:
9
+ Max: 15
10
+
11
+ RSpec/MultipleExpectations:
12
+ Max: 3
13
+
14
+ Style/RescueStandardError:
15
+ Exclude:
16
+ - "*/**/*_spec.rb"
17
+
18
+ Naming/UncommunicativeMethodParamName:
19
+ AllowedNames:
20
+ # These are the default allowed names, set by Rubocop
21
+ - io
22
+ - id
23
+ # These are some custom names that we want to allow, since they aren't
24
+ # uncommunicative - they're actually rather meaningful!
25
+ - as
@@ -0,0 +1 @@
1
+ 2.6.2
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ prometheus-client-tracer (1.0.0)
5
+ prometheus-client (~> 0.10.0.alpha)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ coderay (1.1.2)
12
+ diff-lcs (1.3)
13
+ gc_ruboconfig (2.4.0)
14
+ rubocop (>= 0.70)
15
+ rubocop-rspec (>= 1.33.0)
16
+ jaro_winkler (1.5.3)
17
+ method_source (0.9.2)
18
+ parallel (1.17.0)
19
+ parser (2.6.3.0)
20
+ ast (~> 2.4.0)
21
+ prometheus-client (0.10.0.pre.alpha.2)
22
+ pry (0.12.2)
23
+ coderay (~> 1.1.0)
24
+ method_source (~> 0.9.0)
25
+ rainbow (3.0.0)
26
+ rspec (3.8.0)
27
+ rspec-core (~> 3.8.0)
28
+ rspec-expectations (~> 3.8.0)
29
+ rspec-mocks (~> 3.8.0)
30
+ rspec-core (3.8.2)
31
+ rspec-support (~> 3.8.0)
32
+ rspec-expectations (3.8.4)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.8.0)
35
+ rspec-mocks (3.8.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.8.0)
38
+ rspec-support (3.8.2)
39
+ rubocop (0.72.0)
40
+ jaro_winkler (~> 1.5.1)
41
+ parallel (~> 1.10)
42
+ parser (>= 2.6)
43
+ rainbow (>= 2.2.2, < 4.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (>= 1.4.0, < 1.7)
46
+ rubocop-rspec (1.33.0)
47
+ rubocop (>= 0.60.0)
48
+ ruby-progressbar (1.10.1)
49
+ unicode-display_width (1.6.0)
50
+
51
+ PLATFORMS
52
+ ruby
53
+
54
+ DEPENDENCIES
55
+ gc_ruboconfig (= 2.4.0)
56
+ prometheus-client-tracer!
57
+ pry (~> 0.10)
58
+ rspec (~> 3.8)
59
+
60
+ BUNDLED WITH
61
+ 2.0.1
@@ -0,0 +1,64 @@
1
+ # prometheus-client-tracer-ruby [![CircleCI](https://circleci.com/gh/lawrencejones/prometheus-client-tracer-ruby.svg?style=svg)](https://circleci.com/gh/lawrencejones/prometheus-client-tracer-ruby)
2
+
3
+ This gem provides an interface for tracing long-running duration measurements
4
+ using the Prometheus client. By updating the associated metric as part of the
5
+ Prometheus /metrics endpoint, the increment is spread evenly over the scrapes
6
+ that occur while the operation is in process, instead of applying one massive
7
+ increment at the end.
8
+
9
+ This means you can trust your metrics will be accurate, even if the operations
10
+ you're measuring far exceed your scrape duration.
11
+
12
+ ## Why?
13
+
14
+ One of the most common observability requirements is to trace the amount of time
15
+ spent running a job or task. This information is useful in the context of when
16
+ this time was spent. The typical implementation of such a measurement might be:
17
+
18
+ ```ruby
19
+ def run
20
+ start = Time.now
21
+ long_running_task
22
+ metric.increment(by: start - Time.now)
23
+ end
24
+ ```
25
+
26
+ The metric tracking duration is incremented only once the task has finished. If
27
+ the task took > Prometheus scrape interval, perhaps by a significant factor (say
28
+ 1hr vs the normal 15s scrape interval), then the metric is incremented by a huge
29
+ value only after the task has finished. Graphs visualising how time is spent
30
+ will show no activity while the task was running and then an impossible burst
31
+ just as things finish.
32
+
33
+ To avoid this time of measurement bias, this commit introduces a metric tracer
34
+ that manages updating metrics associated with long-running tasks. Users will
35
+ begin a trace, do work, and while the work is on-going any calls to the tracer
36
+ collect method will incrementally update the associated metric with elapsed
37
+ time.
38
+
39
+ This is a game-changer in terms of making metrics usable, removing a huge source
40
+ of uncertainty when interpreting metric measurements. The implementation of the
41
+ tracer is thread safe and designed to be as lightweight as possible- it should
42
+ hopefully provide no performance hit and be preferable for tracing durations of
43
+ most lengths, provided you don't exceed hundreds of on-going traces.
44
+
45
+ For easy use, the Prometheus client initialises a global tracer in the same vein
46
+ as the global registry. Most users are going to want to use a tracer without
47
+ initialising a handle in their own code, and can do so like this:
48
+
49
+ ```ruby
50
+ def run
51
+ Prometheus::Client.trace(metric, { worker: 1 }) do
52
+ long_running_task
53
+ end
54
+ end
55
+ ```
56
+
57
+ By default, users who do this will see their metrics update just as frequently
58
+ as the original implementation. For the incremental measurement to work, they
59
+ must use a TraceCollector to trigger a collection just prior to serving metrics:
60
+
61
+ ```ruby
62
+ # By default, collect the global tracer
63
+ use Prometheus::Middleware::TraceCollector
64
+ ```
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prometheus
4
+ module Client
5
+ # Most people will want to use a global tracer instead of building their own, similar
6
+ # to how most will use the global metrics registry.
7
+ def self.tracer
8
+ @tracer ||= Tracer.new
9
+ end
10
+
11
+ # Delegate to the Tracer.
12
+ def self.trace(metric, labels = {}, &block)
13
+ tracer.trace(metric, labels, &block)
14
+ end
15
+
16
+ # For metrics that track durations over the course of long-running tasks, we need to
17
+ # ensure the metric is updated gradually while they execute instead of right at the
18
+ # end. This class is used to track on-going 'traces', records of tasks starting and
19
+ # stopping, and exposes a method `collect` that will update the associated metric with
20
+ # the time elapsed since the last `collect`.
21
+ class Tracer
22
+ Trace = Struct.new(:metric, :labels, :time)
23
+
24
+ def initialize
25
+ @lock = Mutex.new
26
+ @traces = []
27
+ end
28
+
29
+ # Start and manage the life of a trace. Pass a long-running block to this method to
30
+ # ensure the associated metric is updated gradually throughout the execution.
31
+ def trace(metric, labels = {})
32
+ start(metric, labels)
33
+ yield
34
+ ensure
35
+ stop(metric, labels)
36
+ end
37
+
38
+ # Update currently traced metrics- this will increment all on-going traces with the
39
+ # delta of time between the last update and now. This should be called just before
40
+ # serving a /metrics request.
41
+ def collect(traces = @traces)
42
+ @lock.synchronize do
43
+ now = monotonic_now
44
+ traces.each do |trace|
45
+ time_since = [now - trace.time, 0].max
46
+ trace.time = now
47
+ trace.metric.increment(
48
+ by: time_since,
49
+ labels: trace.labels,
50
+ )
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def start(metric, labels = {})
58
+ @lock.synchronize { @traces << Trace.new(metric, labels, monotonic_now) }
59
+ end
60
+
61
+ def stop(metric, labels = {})
62
+ matching = nil
63
+ @lock.synchronize do
64
+ matching, @traces = @traces.partition do |trace|
65
+ trace.metric == metric && trace.labels == labels
66
+ end
67
+ end
68
+
69
+ collect(matching)
70
+ end
71
+
72
+ # We're doing duration arithmetic which should make use of monotonic clocks, to
73
+ # avoid changes to the system time from affecting our metrics.
74
+ def monotonic_now
75
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prometheus/client"
4
+
5
+ module Prometheus
6
+ module Middleware
7
+ # This class integrates with a Prometheus::Client::Tracer to update associated metric
8
+ # traces just prior to serving metrics. By default, this will collect traces on the
9
+ # global Client tracer.
10
+ class TraceCollector
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ @tracer = options[:tracer] || Client.tracer
14
+ end
15
+
16
+ def call(env)
17
+ @tracer.collect
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "prometheus-client-tracer"
8
+ spec.version = "1.0.0"
9
+ spec.summary = "Tracer for accurate duration tracking with Prometheus metrics"
10
+ spec.authors = %w[me@lawrencejones.dev]
11
+ spec.homepage = "https://github.com/lawrencejones/prometheus-client-tracer"
12
+ spec.email = %w[me@lawrencejones.dev]
13
+ spec.license = "MIT"
14
+
15
+ spec.required_ruby_version = ">= 2.4"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "prometheus-client", "~> 0.10.0.alpha"
22
+
23
+ spec.add_development_dependency "gc_ruboconfig", "= 2.4.0"
24
+ spec.add_development_dependency "pry", "~> 0.10"
25
+ spec.add_development_dependency "rspec", "~> 3.8"
26
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ # rubocop:disable RSpec/InstanceVariable
6
+ describe Prometheus::Client::Tracer do
7
+ subject(:tracer) { described_class.new }
8
+
9
+ let(:metric) do
10
+ Prometheus::Client::Counter.new(:counter, docstring: "example", labels: %i[worker])
11
+ end
12
+
13
+ describe ".trace" do
14
+ # These tests try stubbing timing logic. Instead of using let's, we use a test
15
+ # instance variable @now to represent the current time, as returned by a monotonic
16
+ # clock system call.
17
+ #
18
+ # The #monotonic_now method of the tracer is stubbed to always return the current
19
+ # value of @now, and tests can manipulate time by advancing that instance variable.
20
+ #
21
+ # The tracer is normally passed a block that manipulates @now.
22
+ subject(:trace) { tracer.trace(metric, labels, &trace_block) }
23
+
24
+ let(:labels) { { worker: 1 } }
25
+ let(:trace_block) { -> { @now += 1.0 } }
26
+
27
+ before do
28
+ @now = 0.0 # set initial time
29
+ allow(tracer).to receive(:monotonic_now) { @now }
30
+ end
31
+
32
+ it "increments metric with elapsed duration" do
33
+ expect { trace }.to change { metric.values[labels] }.by(1.0)
34
+ end
35
+
36
+ context "when .collect is called during a trace" do
37
+ let(:latch) { Mutex.new }
38
+ let(:trace_block) do
39
+ -> { latch.synchronize { @now += 1.0 } }
40
+ end
41
+
42
+ # rubocop:disable RSpec/ExampleLength
43
+ it "increments metric with incremental duration" do
44
+ latch.lock # acquire the lock, trace should now block
45
+ trace_thread = Thread.new do
46
+ trace # will block until latch is released
47
+ end
48
+
49
+ # We need to block until the trace_thread has actually begun, otherwise we'll
50
+ # never be able to guarantee the trace was started at now = 0.0, even if this
51
+ # should happen almost immediately.
52
+ Timeout.timeout(1) { sleep(0.01) until tracer.collect.any? }
53
+
54
+ # Advance the clock by 0.5s
55
+ @now += 0.5
56
+
57
+ # If we now collect, the metric should be incremented by the elapsed time (0.5s)
58
+ expect { tracer.collect }.to change { metric.values[labels] }.by(0.5)
59
+
60
+ # Collect once more should leave the metric unchanged, as no time has passed since
61
+ # the last collect
62
+ expect { tracer.collect }.to change { metric.values[labels] }.by(0.0)
63
+
64
+ # Unlocking the latch and allowing the trace thread to complete will execute the
65
+ # final part of a trace, which should update the metric with time elapsed. The
66
+ # trace thread advances time by 1s right before it ends, so we expect to update
67
+ # the metric by 1s.
68
+ #
69
+ # A bug would be if we incremented the metric by the time since our trace started
70
+ # and when it ended, which in total is 1.5s. This would suggest we never reset the
71
+ # trace clock when calling collect.
72
+ expect do
73
+ latch.unlock
74
+ trace_thread.join(1)
75
+ trace_thread.kill # in case thread misbehaves
76
+ end.to change { metric.values[labels] }.by(1.0)
77
+ end
78
+ # rubocop:enable RSpec/ExampleLength
79
+ end
80
+ end
81
+ end
82
+ # rubocop:enable RSpec/InstanceVariable
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Prometheus::Middleware::TraceCollector do
4
+ subject(:collector) { described_class.new(app, options) }
5
+
6
+ let(:app) { instance_double(Proc, call: []) }
7
+ let(:options) { { tracer: tracer } }
8
+ let(:tracer) { Prometheus::Client::Tracer.new }
9
+
10
+ describe ".call" do
11
+ subject(:call) { collector.call({}) }
12
+
13
+ # The most basic of tests, just verifying the tracer is invoked. We rely on the tests
14
+ # for the tracer to validate #collect works correctly.
15
+ it "calls tracer.collect, then the original app" do
16
+ expect(tracer).to receive(:collect).and_call_original
17
+ expect(app).to receive(:call)
18
+
19
+ call
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prometheus/client"
4
+ require "prometheus/client/tracer"
5
+ require "prometheus/middleware/trace_collector"
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prometheus-client-tracer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - me@lawrencejones.dev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prometheus-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.10.0.alpha
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.10.0.alpha
27
+ - !ruby/object:Gem::Dependency
28
+ name: gc_ruboconfig
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 2.4.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 2.4.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ description:
70
+ email:
71
+ - me@lawrencejones.dev
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".circleci/config.yml"
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - ".ruby-version"
80
+ - Gemfile
81
+ - Gemfile.lock
82
+ - README.md
83
+ - lib/prometheus/client/tracer.rb
84
+ - lib/prometheus/middleware/trace_collector.rb
85
+ - prometheus-client-tracer.gemspec
86
+ - spec/prometheus/client/tracer_spec.rb
87
+ - spec/prometheus/middleware/trace_collector_spec.rb
88
+ - spec/spec_helper.rb
89
+ homepage: https://github.com/lawrencejones/prometheus-client-tracer
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '2.4'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.0.3
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Tracer for accurate duration tracking with Prometheus metrics
112
+ test_files:
113
+ - spec/prometheus/client/tracer_spec.rb
114
+ - spec/prometheus/middleware/trace_collector_spec.rb
115
+ - spec/spec_helper.rb