prometheus-client-tracer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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