emrb 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67a0d802b62cf9845b629d83b043030c9140e9c8d931c7937aaa03ad3dec1f27
4
+ data.tar.gz: 425652e89206e2e1b5ad1e714b49ce7928302cb87616d1def2c8bc6b642a1c80
5
+ SHA512:
6
+ metadata.gz: 42efa7169bf01425067f988fb20c0c54c9a1c87a149dca417b3f14f2d0fc4b6a7d5bed286ee16b51ac0745009f5cfbc9712d0c441a2b56ea09924279af0325ff
7
+ data.tar.gz: 6fc41dfa38ab82be8d310d7fd04ac4c8530f2a3c3c8a7ad9d3766795df7b1ac0f13ef272b6a2e452a4eba8773094405a6a834308d06f9a21fa47bb4626df54c7
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ SuggestExtensions: false
3
+ TargetRubyVersion: 3.2
4
+ NewCops: enable
5
+ Exclude:
6
+ - example/**/*
7
+
8
+ Style/StringLiterals:
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ EnforcedStyle: double_quotes
13
+
14
+ Layout/LineLength:
15
+ Max: 120
16
+
17
+ Metrics/BlockLength:
18
+ Exclude:
19
+ - spec/**/**.rb
20
+
21
+ Layout/FirstHashElementIndentation:
22
+ EnforcedStyle: consistent
23
+
24
+ Layout/FirstArrayElementIndentation:
25
+ EnforcedStyle: consistent
26
+
27
+ Layout/ArgumentAlignment:
28
+ EnforcedStyle: with_fixed_indentation
29
+
30
+ Layout/MultilineMethodCallIndentation:
31
+ EnforcedStyle: indented
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # Easy Metrics (rb)
2
+ This project provides a facility for instrumenting applications with [Prometheus](https://github.com/prometheus/client_ruby) metrics.
3
+
4
+
5
+ ## Usage
6
+ ### [Examples TL;DR](./example)
7
+
8
+ Add the gem to your project:
9
+ ```
10
+ bundle add emrb
11
+ ```
12
+
13
+
14
+ ### Basic usage
15
+
16
+ ```ruby
17
+ require "emrb"
18
+
19
+ class Metrics
20
+ # Emrb::Instruments provides all necessary methods for creating instruments.
21
+ include Emrb::Instruments
22
+ # Instruments can be defined as the following:
23
+ counter :my_counter
24
+ end
25
+
26
+ # Perform measurements.
27
+ Metrics.my_counter.inc
28
+ ```
29
+
30
+ ## Features
31
+ ### Supported metrics
32
+
33
+ All [Prometheus metrics](https://github.com/prometheus/client_ruby?tab=readme-ov-file#metrics)
34
+ are supported and can be created the same way as in the example above.
35
+
36
+ ### Providing metric-specific configs
37
+
38
+ Most of the provided methods are nothing but a shorthand for creating instruments (with some facilities):
39
+ ```ruby
40
+ require "emrb"
41
+
42
+ class Metrics
43
+ include Emrb::Instruments
44
+ # /-> All other options for each metric
45
+ # | follow the original client's syntax.
46
+ counter :my_counter, "this is my counter", labels: [:my_label]
47
+ # _________________/
48
+ # \_> This is the "docstring" value used for creating
49
+ # metrics using the Prometheus client.
50
+ # This is the only API-breaking option
51
+ # for enhancing usability. Defaults to "..." when absent.
52
+ end
53
+ ```
54
+
55
+ When your metric requires multiple options, a single line may become unwieldy. In such cases, you can use a block to define the metric config:
56
+
57
+ ```ruby
58
+ require "emrb"
59
+
60
+ class Metrics
61
+ include Emrb::Instruments
62
+
63
+ # The block should return a hash containing all the desired options.
64
+ # When a block is used, other options passed inline are ignored.
65
+ histogram :my_histogram do
66
+ {
67
+ labels: [:label1, :label2],
68
+ preset_labels: { label1: "bar" },
69
+ buckets: [0.9, 1.0, ...]
70
+ }
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### Presets
76
+ If more than one of your metrics leverages the same `preset_labels`, the following is possible:
77
+
78
+ ```ruby
79
+ require "emrb"
80
+
81
+ class Metrics
82
+ include Emrb::Instruments
83
+
84
+ # Apply preset labels to multiple metrics within a block.
85
+ with_presets label_a: "value_a", label_b: "value_b" do
86
+ counter :counter_a
87
+ counter :counter_b, labels: [:label_c]
88
+ # \_> You can continue configuring
89
+ # each metric as usual.
90
+ # The preset labels will be merged
91
+ # with the specific ones.
92
+ end
93
+ end
94
+ # Metrics.counter_a.labels => [:label_a, :label_b]
95
+ # Metrics.counter_b.labels => [:label_a, :label_b, :label_c]
96
+ ```
97
+
98
+ ### Subsystems
99
+
100
+ You can organize your metrics into subsystems, allowing different components of your application to group related metrics:
101
+
102
+ ```ruby
103
+ require "emrb"
104
+
105
+ class Metrics
106
+ include Emrb::Instruments
107
+ subsystem :http do
108
+ histogram :request_duration, "duration of requests" do
109
+ { labels: [:method, :path] }
110
+ end
111
+ end
112
+
113
+ subsystem :postgres do
114
+ # / -> Subsystems also accepts presets.
115
+ # |
116
+ subsystem :replica, op: "read" do
117
+ counter :op_count
118
+ end
119
+
120
+ subsystem :master do
121
+ counter :op_count, labels: [:op]
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ Metrics within a subsystem are scoped by the subsystem's name. To access metrics within a subsystem:
128
+
129
+ ```ruby
130
+ Metrics.postgres.master.op_count
131
+ ```
132
+
133
+ ### Subsystem's metrics name
134
+
135
+ Metrics created within subsystems are prefixed with the subsystem's name. For example:
136
+ * The `request_duration` metric within the `http` subsystem will be named `http_request_duration`
137
+ * The `op_count` metric of `postgres.master` will be named `postgres_master_op_count`.
138
+
139
+ ## Exposing metrics
140
+
141
+ Both [`Prometheus::Middleware::Exporter`](https://github.com/prometheus/client_ruby/blob/main/lib/prometheus/middleware/exporter.rb)
142
+ and [`Prometheus::Middleware::Collector`](https://github.com/prometheus/client_ruby/blob/main/lib/prometheus/middleware/collector.rb)
143
+ are [Rack middlewares](https://github.com/rack/rack) included in this gem as `Emrb::Exporter` and `Emrb::Collector`.
144
+ <br>
145
+
146
+ If you're unfamiliar with how to use these middlewares, the [http example](./example/http/main.rb) provides a demonstration.
147
+
148
+ ## Pushing metrics
149
+
150
+ ```ruby
151
+ require "emrb"
152
+
153
+ class Metrics
154
+ include Emrb::Instruments
155
+ counter :my_counter
156
+ end
157
+
158
+ # Perform measurements.
159
+ Metrics.my_counter.inc
160
+
161
+ # Push the current registry state to your gateway.
162
+ Metrics.push("example", gateway: "http://localhost:9091")
163
+ ```
164
+
165
+ ## License
166
+ ```
167
+ The MIT License (MIT)
168
+
169
+ Copyright (c) 2024 Felipe Mariotti, Vito Sartori
170
+
171
+ Permission is hereby granted, free of charge, to any person obtaining a copy
172
+ of this software and associated documentation files (the "Software"), to deal
173
+ in the Software without restriction, including without limitation the rights
174
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
175
+ copies of the Software, and to permit persons to whom the Software is
176
+ furnished to do so, subject to the following conditions:
177
+
178
+ The above copyright notice and this permission notice shall be included in
179
+ all copies or substantial portions of the Software.
180
+
181
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
182
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
183
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
184
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
185
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
186
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
187
+ THE SOFTWARE.
188
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "emrb", path: "../../"
6
+
7
+ gem "sinatra", "~> 4.0"
8
+
9
+ gem "rackup", "~> 2.1"
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ emrb (0.1.0)
5
+ prometheus-client (~> 4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ base64 (0.2.0)
11
+ mustermann (3.0.3)
12
+ ruby2_keywords (~> 0.0.1)
13
+ prometheus-client (4.2.3)
14
+ base64
15
+ rack (3.1.7)
16
+ rack-protection (4.0.0)
17
+ base64 (>= 0.1.0)
18
+ rack (>= 3.0.0, < 4)
19
+ rack-session (2.0.0)
20
+ rack (>= 3.0.0)
21
+ rackup (2.1.0)
22
+ rack (>= 3)
23
+ webrick (~> 1.8)
24
+ ruby2_keywords (0.0.5)
25
+ sinatra (4.0.0)
26
+ mustermann (~> 3.0)
27
+ rack (>= 3.0.0, < 4)
28
+ rack-protection (= 4.0.0)
29
+ rack-session (>= 2.0.0, < 3)
30
+ tilt (~> 2.0)
31
+ tilt (2.4.0)
32
+ webrick (1.8.1)
33
+
34
+ PLATFORMS
35
+ ruby
36
+ x86_64-linux
37
+
38
+ DEPENDENCIES
39
+ emrb!
40
+ rackup (~> 2.1)
41
+ sinatra (~> 4.0)
42
+
43
+ BUNDLED WITH
44
+ 2.5.18
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emrb"
4
+ require "sinatra/base"
5
+
6
+ class App < Sinatra::Base
7
+ class Metrics
8
+ include Emrb::Instruments
9
+ histogram :request_duration, "Request duration" do
10
+ { labels: [:path, :method], buckets: [0.1, 0.2] }
11
+ end
12
+ end
13
+
14
+ # Metrics will be exposed at /metrics
15
+ use Emrb::Exporter
16
+
17
+ def measure_request_duration
18
+ labels = { path: request.path, method: request.env["REQUEST_METHOD"].downcase }
19
+ now = Time.now
20
+ yield
21
+
22
+ ensure
23
+ Metrics.request_duration.observe(Time.now - now, labels:)
24
+ end
25
+
26
+ get "/" do
27
+ measure_request_duration { [200, "OK" ] }
28
+ end
29
+ end
30
+
31
+ App.run!
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "emrb", path: "../../"
@@ -0,0 +1,22 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ emrb (0.1.0)
5
+ prometheus-client (~> 4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ base64 (0.2.0)
11
+ prometheus-client (4.2.3)
12
+ base64
13
+
14
+ PLATFORMS
15
+ ruby
16
+ x86_64-linux
17
+
18
+ DEPENDENCIES
19
+ emrb!
20
+
21
+ BUNDLED WITH
22
+ 2.5.18
@@ -0,0 +1,15 @@
1
+ services:
2
+ prom-pushgateway:
3
+ image: prom/pushgateway
4
+ ports:
5
+ - 9091:9091
6
+ prometheus:
7
+ image: prom/prometheus
8
+ volumes:
9
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
10
+ command:
11
+ - '--config.file=/etc/prometheus/prometheus.yml'
12
+ - '--storage.tsdb.path=/prometheus'
13
+ ports:
14
+ - 9090:9090
15
+ network_mode: host
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emrb"
4
+
5
+ class Job
6
+ class Metrics
7
+ include Emrb::Instruments
8
+ counter :done_stuff, "how much stuff was done"
9
+ end
10
+
11
+ def do_stuff
12
+ # Do stuff
13
+ Metrics.done_stuff.inc
14
+ Metrics.push("example", gateway: "http://localhost:9091")
15
+ end
16
+ end
17
+
18
+ j = Job.new
19
+
20
+ t = Thread.new do
21
+ 10.times do
22
+ j.do_stuff
23
+ sleep 1
24
+ end
25
+ end
26
+
27
+ t.join
@@ -0,0 +1,10 @@
1
+ global:
2
+ scrape_interval: 3s
3
+ scrape_configs:
4
+ - job_name: dev-push-gateway
5
+ metrics_path: /metrics
6
+ scheme: http
7
+ static_configs:
8
+ - targets: ['localhost:9091']
9
+ labels:
10
+ service: 'prom-pushgateway'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prometheus
4
+ module Client
5
+ # Internal: Extension of Prometheus::Client::Counter
6
+ # that can be used for implementing facilities.
7
+ class Counter
8
+ alias inc increment
9
+ end
10
+
11
+ # Internal: Extension of Prometheus::Client::Gauge
12
+ # that can be used for implementing facilities.
13
+ class Gauge
14
+ alias inc increment
15
+ alias dec decrement
16
+ end
17
+
18
+ # Internal: Extension of Prometheus::Client::Histogram
19
+ # that can be used for implementing facilities.
20
+ class Histogram
21
+ alias obs observe
22
+ end
23
+
24
+ # Internal: Extension of Prometheus::Client::Summary
25
+ # that can be used for implementing facilities.
26
+ class Summary
27
+ alias obs observe
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emrb
4
+ # Rack middleware that provides a sample implementation of a
5
+ # Prometheus HTTP exposition endpoint.
6
+ #
7
+ # By default it will export the state of the global registry and expose it
8
+ # under `/metrics`. Use the `:registry` and `:path` options to change the
9
+ # defaults.
10
+ # Original source: https://github.com/prometheus/client_ruby/blob/main/lib/prometheus/middleware/exporter.rb
11
+ Exporter = Prometheus::Middleware::Exporter
12
+
13
+ # Rack middleware that provides a sample implementation of a
14
+ # HTTP tracer.
15
+ #
16
+ # By default metrics are registered on the global registry. Set the
17
+ # `:registry` option to use a custom registry.
18
+ #
19
+ # By default metrics all have the prefix "http_server". Set
20
+ # `:metrics_prefix` to something else if you like.
21
+ #
22
+ # The request counter metric is broken down by code, method and path.
23
+ # The request duration metric is broken down by method and path.
24
+ # Original source: https://github.com/prometheus/client_ruby/blob/main/lib/prometheus/middleware/collector.rb
25
+ Collector = Prometheus::Middleware::Collector
26
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emrb
4
+ # Instruments provide the class methods to create metrics.
5
+ module Instruments
6
+ def self.included(base) = base.extend(ClassMethods)
7
+
8
+ # CollidingNameError indicates that a declared instrument will clash with a library method. Use another name.
9
+ class CollidingNameError < StandardError
10
+ def initialize(name) = super("Identifying an instrument with #{name} will override the class method")
11
+ end
12
+
13
+ # Provides the instrumentation facilities when including/extending Instruments.
14
+ module ClassMethods
15
+ FORBIDDEN_IDENTIFIERS = instance_methods
16
+
17
+ # Initializes a new Prometheus counter using the provided identifier, documentation,
18
+ # and either optional keyword arguments or a block that returns the keyword arguments.
19
+ # The block takes precedence when supplied.
20
+ #
21
+ # identifier - The counter identifier.
22
+ # docs - The instrument docstring. Defaults to "..."
23
+ #
24
+ # Usage example:
25
+ #
26
+ # class MyApp
27
+ # class Metrics
28
+ # include Emrb::Instruments
29
+ # counter :visits, "Number of visits the app has received"
30
+ # end
31
+ #
32
+ # def handle_visits
33
+ # Metrics.visits.inc
34
+ # ...
35
+ # end
36
+ # end
37
+ #
38
+ # For a complete list of options and functionalities for creating and utilizing a Counter,
39
+ # refer to the Prometheus client documentation:
40
+ # https://github.com/prometheus/client_ruby?tab=readme-ov-file#counter
41
+ def counter(identifier, docs = "...", **, &)
42
+ check_identifier!(identifier)
43
+ opts = block_opts(**, &)
44
+ State.counter(id_for(identifier), docs, **opts).tap do |c|
45
+ define_singleton_method(identifier) { c }
46
+ end
47
+ end
48
+
49
+ # Initializes a new Prometheus gauge using the provided identifier, documentation,
50
+ # and either optional keyword arguments or a block that returns the keyword arguments.
51
+ # The block takes precedence when supplied.
52
+ #
53
+ # identifier - The counter identifier.
54
+ # docs - The instrument docstring. Defaults to "..."
55
+ #
56
+ # Usage example:
57
+ #
58
+ # class Thermometer
59
+ # class Metrics
60
+ # include Emrb::Instruments
61
+ # gauge :current_temperature, "current temperature"
62
+ # end
63
+ #
64
+ # def set_current_temp
65
+ # temp = < ... >
66
+ # Metrics.current_temperature.set(temp)
67
+ # end
68
+ # end
69
+ #
70
+ # For a full list of options and functionalities for creating and utilizing a Gauge,
71
+ # refer to the Prometheus client documentation:
72
+ # https://github.com/prometheus/client_ruby?tab=readme-ov-file#gauge
73
+ def gauge(identifier, docs, **, &)
74
+ check_identifier!(identifier)
75
+ opts = block_opts(**, &)
76
+ State.gauge(id_for(identifier), docs, **opts).tap do |g|
77
+ define_singleton_method(identifier) { g }
78
+ end
79
+ end
80
+
81
+ # Initializes a new Prometheus histogram using the provided identifier, documentation,
82
+ # and either optional keyword arguments or a block that returns the keyword arguments.
83
+ # The block takes precedence when supplied.
84
+ #
85
+ # identifier - The counter identifier.
86
+ # docs - The instrument docstring. Defaults to "..."
87
+ #
88
+ # Usage example:
89
+ #
90
+ # class MyApp
91
+ # class Metrics
92
+ # include Emrb::Instruments
93
+ # histogram :request_duration, "Duration of requests" do
94
+ # { labels: [:path, :method], buckets: [0.1, 0.2] }
95
+ # end
96
+ # end
97
+ #
98
+ # def measure_request_duration
99
+ # labels = { path: request.path, method: request.env["REQUEST_METHOD"].downcase } #
100
+ # Metrics.request_duration.observe(Benchmark.realtime { yield }, labels: )
101
+ # end
102
+ #
103
+ # get "/" do
104
+ # measure_request_duration { [200, "OK"] }
105
+ # end
106
+ # end
107
+ #
108
+ # For all available options and functionalities for creating and utilizing a Histogram,
109
+ # refer to the Prometheus client documentation:
110
+ # https://github.com/prometheus/client_ruby?tab=readme-ov-file#histogram
111
+ def histogram(identifier, docs, **, &)
112
+ check_identifier!(identifier)
113
+ opts = block_opts(**, &)
114
+ State.histogram(id_for(identifier), docs, **opts).tap do |h|
115
+ define_singleton_method(identifier) { h }
116
+ end
117
+ end
118
+
119
+ # Initializes a new Prometheus summary using the provided identifier, documentation,
120
+ # and either optional keyword arguments or a block that returns the keyword arguments.
121
+ # The block takes precedence when supplied.
122
+ #
123
+ # identifier - The counter identifier.
124
+ # docs - The instrument docstring. Defaults to "..."
125
+ #
126
+ # Usage example:
127
+ #
128
+ # class MyApp
129
+ # class Metrics
130
+ # include Emrb::Instruments
131
+ # summary :call_duration, "Duration of a given call"
132
+ # end
133
+
134
+ # def do_a_call
135
+ # Metrics.call_duration.observe(Benchmark.realtime { < ... > })
136
+ # end
137
+ # end
138
+ #
139
+ # For a complete list of options and functionalities for creating and utilizing a Summary,
140
+ # refer to the Prometheus client documentation:
141
+ # https://github.com/prometheus/client_ruby?tab=readme-ov-file#summary
142
+ def summary(identifier, docs, **, &)
143
+ check_identifier!(identifier)
144
+ opts = block_opts(**, &)
145
+ State.summary(id_for(identifier), docs, **opts).tap do |s|
146
+ define_singleton_method(identifier) { s }
147
+ end
148
+ end
149
+
150
+ # push the current registry state to a Pushgateway.
151
+ # It receives an obligatory job identifier, and optionally all supported
152
+ # keyword arguments of a Prometheus::Client::Push.
153
+ #
154
+ # job - Job identifier
155
+ #
156
+ # Usage example:
157
+ #
158
+ # class MyJob
159
+ # class Metrics
160
+ # include Emrb::Instruments
161
+ # counter :processed_tasks
162
+ # end
163
+ #
164
+ # def do_the_things
165
+ # begin
166
+ # tasks.each do |t|
167
+ # < ... >
168
+ # Metrics.processed_tasks.inc
169
+ # end
170
+ # rescue
171
+ # < ... >
172
+ # ensure
173
+ # Metrics.push("job_name")
174
+ # end
175
+ # end
176
+ # end
177
+ def push(job, **) = State.push(job, **)
178
+
179
+ # Allows instruments to be declared with preset labels.
180
+ #
181
+ # labels - A hash containing the preset labels and values
182
+ #
183
+ # Usage example:
184
+ #
185
+ # class Metrics
186
+ # include Emrb::Instruments
187
+ # with_presets my: "label", other: "label" do
188
+ # counter :my_counter, "a counter"
189
+ # end
190
+ # end
191
+ #
192
+ # Metrics.my_counter.labels => [:my, :other]
193
+ # Metrics.my_counter.preset_labels => { my: "label", other: "label" }
194
+ def with_presets(**labels, &)
195
+ raise LocalJumpError, "no block given" unless block_given?
196
+ raise ArgumentError, "labels are empty" if labels.nil? || labels.empty?
197
+
198
+ old = (@presets ||= {})
199
+ current = old.merge labels
200
+ @presets = current
201
+ instance_eval(&)
202
+ @presets = old
203
+ end
204
+
205
+ # Provides a way to compose metrics for different subsystems.
206
+ # All instruments created within a subsystem will have their identifiers
207
+ # prefixed with the prefix param.
208
+ #
209
+ # prefix - determines the prefix of all instruments declared
210
+ # within the subsystem and how to access those instruments
211
+ # within the implementation.
212
+ #
213
+ # inherit_presets - determines whether or not to inherit presets from
214
+ # the parent. Defaults to false.
215
+ #
216
+ # presets - preset of labels to be used by all Instruments
217
+ # of a given subsystem.
218
+ #
219
+ # Usage example:
220
+ #
221
+ # class Metrics
222
+ # subsystem :http do
223
+ # histogram :request_duration, "duration of requests" do
224
+ # { labels: [:method, :path] }
225
+ # end
226
+ # end
227
+ #
228
+ # subsystem :postgres do
229
+ # subsystem :master, op: "write" do
230
+ # counter :op_count
231
+ # end
232
+ #
233
+ # subsystem :replica, op: "read" do
234
+ # counter :op_count
235
+ # end
236
+ # end
237
+ # end
238
+ #
239
+ # # Acessing instruments:
240
+ # Metrics.http.request_duration
241
+ # Metrics.postgres.master.op_count
242
+ # Metrics.postgres.replica.op_count
243
+ # rubocop:disable Metrics/MethodLength
244
+ def subsystem(prefix, inherit_presets: false, **presets, &)
245
+ raise LocalJumpError, "no block given" unless block_given?
246
+
247
+ s_prefix = prefix.to_s
248
+ s_prefix.delete_suffix! "_" if s_prefix.end_with? "_"
249
+ subsystem = id_for(s_prefix.to_sym)
250
+
251
+ presets.merge! @presets if inherit_presets && !@presets.nil?
252
+
253
+ s = Class.new
254
+ s.include(Instruments)
255
+ s.instance_variable_set(:@subsystem, subsystem)
256
+ s.instance_variable_set(:@presets, presets)
257
+ s.instance_eval(&)
258
+ define_singleton_method(prefix) { s }
259
+ end
260
+ # rubocop:enable Metrics/MethodLength
261
+
262
+ # Internal: validates whether to concatenate the given identifier with
263
+ # a pre-existing susbsystem name.
264
+ def id_for(identifier)
265
+ return identifier unless @subsystem
266
+
267
+ :"#{@subsystem}_#{identifier}"
268
+ end
269
+
270
+ # Internal: Validates the provided options to create a given instrument
271
+ # and performs the merge of all options with presets when they are present.
272
+ def block_opts(**opts, &)
273
+ res = block_given? ? yield : opts
274
+ return res if @presets.nil?
275
+
276
+ res[:labels] = [] if res[:labels].nil?
277
+ res[:labels].append(*@presets.keys)
278
+
279
+ if res[:preset_labels].nil?
280
+ res[:preset_labels] = @presets
281
+ return res
282
+ end
283
+
284
+ res[:preset_labels].merge!(**@presets)
285
+ res
286
+ end
287
+
288
+ # Internal: Validates whether the given identifier might override one of the class methods.
289
+ # In a positive case, raises CollidingNameError.
290
+ def check_identifier!(id)
291
+ raise Instruments::CollidingNameError, id if FORBIDDEN_IDENTIFIERS.include? id
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emrb
4
+ module Instruments
5
+ # Internal: State is responsible for interacting with the Prometheus client.
6
+ class State
7
+ # Internal: All supported Prometheus metrics.
8
+ INSTRUMENTS = {
9
+ c: Prometheus::Client::Counter,
10
+ g: Prometheus::Client::Gauge,
11
+ h: Prometheus::Client::Histogram,
12
+ s: Prometheus::Client::Summary
13
+ }.freeze
14
+
15
+ class << self
16
+ # Internal: creates, registers, and returns a new Prometheus::Client::Counter.
17
+ # Requires an instrument identifier and a docstring as mandatory arguments,
18
+ # with optional keyword arguments supported by the Prometheus Counter.
19
+ # Prom docs: https://github.com/prometheus/client_ruby?tab=readme-ov-file#counter
20
+ def counter(identifier, docstring, **)
21
+ INSTRUMENTS[:c].new(identifier, docstring:, **).tap { reg.register _1 }
22
+ end
23
+
24
+ # Internal: creates, registers, and returns a new Prometheus::Client::Gauge.
25
+ # Requires an instrument identifier and a docstring as mandatory arguments,
26
+ # with optional keyword arguments supported by the Prometheus Gauge.
27
+ # Prom docs: https://github.com/prometheus/client_ruby?tab=readme-ov-file#gauge
28
+ def gauge(identifier, docstring, **)
29
+ INSTRUMENTS[:g].new(identifier, docstring:, **).tap { reg.register _1 }
30
+ end
31
+
32
+ # Internal: creates, registers, and returns a new Prometheus::Client::Histogram.
33
+ # Requires an instrument identifier and a docstring as mandatory arguments,
34
+ # with optional keyword arguments supported by the Prometheus Histogram.
35
+ # Prom docs: https://github.com/prometheus/client_ruby?tab=readme-ov-file#histogram
36
+ def histogram(identifier, docstring, **)
37
+ INSTRUMENTS[:h].new(identifier, docstring:, **).tap { reg.register _1 }
38
+ end
39
+
40
+ # Internal: creates, registers, and returns a new Prometheus::Client::Summary.
41
+ # Requires an instrument identifier and a docstring as mandatory arguments,
42
+ # with optional keyword arguments supported by the Prometheus Summary.
43
+ # Prom docs: https://github.com/prometheus/client_ruby?tab=readme-ov-file#summary
44
+ def summary(identifier, docstring, **)
45
+ INSTRUMENTS[:s].new(identifier, docstring:, **).tap { reg.register _1 }
46
+ end
47
+
48
+ # Internal: pushes the current registry state to a Pushgateway.
49
+ # It receives an obligatory job identifier, and optionally all supported
50
+ # keyword arguments of a new push.
51
+ # Prom docs: https://github.com/prometheus/client_ruby?tab=readme-ov-file#pushgateway
52
+ def push(job, **) = Prometheus::Client::Push.new(job:, **).add(reg)
53
+
54
+ # Internal: returns the current client registry.
55
+ def reg = Prometheus::Client.registry
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instruments/state"
4
+ require_relative "instruments/instruments"
5
+ require_relative "instruments/exporters"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emrb
4
+ VERSION = "0.1.0"
5
+ end
data/lib/emrb.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prometheus"
4
+ require "prometheus/client/push"
5
+ require "prometheus/middleware/collector"
6
+ require "prometheus/middleware/exporter"
7
+ require_relative "emrb/ext/prometheus"
8
+
9
+ require_relative "emrb/version"
10
+ require_relative "emrb/instruments"
11
+
12
+ # Emrb provides a facility for instrumenting applications with prometheus metrics.
13
+ # All instruments can be used the same way as in prometheus-client, and are
14
+ # registered within the same registry.
15
+ # Both Prometheus::Middleware::Collector and Prometheus::Middleware::Exporter
16
+ # can be accessed using Emrb::Collector and Emrb::Exporter.
17
+ module Emrb
18
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: emrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Felipe Mariotti
8
+ - Vito Sartori
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2024-09-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: prometheus-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '4'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '4'
28
+ description: Easy prometheus instrumentation for Ruby applications
29
+ email:
30
+ - felipe.mtt95@gmail.com
31
+ - hey@vito.io
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - CODE_OF_CONDUCT.md
39
+ - README.md
40
+ - Rakefile
41
+ - example/http/Gemfile
42
+ - example/http/Gemfile.lock
43
+ - example/http/main.rb
44
+ - example/push/Gemfile
45
+ - example/push/Gemfile.lock
46
+ - example/push/compose.yaml
47
+ - example/push/main.rb
48
+ - example/push/prometheus.yml
49
+ - lib/emrb.rb
50
+ - lib/emrb/ext/prometheus.rb
51
+ - lib/emrb/instruments.rb
52
+ - lib/emrb/instruments/exporters.rb
53
+ - lib/emrb/instruments/instruments.rb
54
+ - lib/emrb/instruments/state.rb
55
+ - lib/emrb/version.rb
56
+ homepage: https://github.com/ofeefo/emrb
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ allowed_push_host: https://rubygems.org
61
+ homepage_uri: https://github.com/ofeefo/emrb
62
+ source_code_uri: https://github.com/ofeefo/emrb
63
+ changelog_uri: https://github.com/ofeefo/emrb
64
+ rubygems_mfa_required: 'true'
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.2'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.5.18
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Easy prometheus instrumentation for Ruby applications
84
+ test_files: []