autometrics 0.0.1
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 +7 -0
- data/.gitignore +56 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +27 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/autometrics.gemspec +33 -0
- data/autometrics_test_quick.rb +52 -0
- data/examples/autometrics-sinatra-example/.gitignore +2 -0
- data/examples/autometrics-sinatra-example/Gemfile +16 -0
- data/examples/autometrics-sinatra-example/Gemfile.lock +40 -0
- data/examples/autometrics-sinatra-example/README.md +40 -0
- data/examples/autometrics-sinatra-example/db.rb +27 -0
- data/examples/autometrics-sinatra-example/suffragist.rb +27 -0
- data/lib/autometrics/logging.rb +26 -0
- data/lib/autometrics/prometheus-client.rb +37 -0
- data/lib/autometrics/version.rb +3 -0
- data/lib/autometrics.rb +158 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 21d41e117081d9491015d27d01e7b4c958c5118487f4eeb49d1aa5e5b7f50226
|
4
|
+
data.tar.gz: f77d2f58af4ec3ba0608d5fae471240c569b993050d0e72072306a0a024df074
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c553bd576b0eeec75da64346d5c79dfb84fe2a0007cc9d1209db2887a21c06dbca8c33d108e0c24843a2be6a4e42f309204a1047300ad3fe6528d9b995c9d5d
|
7
|
+
data.tar.gz: 7c921566a8756de4d04d588238e57e40002b5065961c1620625a8bc93e0838ee311fd21b2b68a00a026703c5f705735af1967b2d2fcdbc6dd06ae6c2d66b73b1
|
data/.gitignore
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
# Ignore Byebug command history file.
|
17
|
+
.byebug_history
|
18
|
+
|
19
|
+
## Specific to RubyMotion:
|
20
|
+
.dat*
|
21
|
+
.repl_history
|
22
|
+
build/
|
23
|
+
*.bridgesupport
|
24
|
+
build-iPhoneOS/
|
25
|
+
build-iPhoneSimulator/
|
26
|
+
|
27
|
+
## Specific to RubyMotion (use of CocoaPods):
|
28
|
+
#
|
29
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
30
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
31
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
32
|
+
#
|
33
|
+
# vendor/Pods/
|
34
|
+
|
35
|
+
## Documentation cache and generated files:
|
36
|
+
/.yardoc/
|
37
|
+
/_yardoc/
|
38
|
+
/doc/
|
39
|
+
/rdoc/
|
40
|
+
|
41
|
+
## Environment normalization:
|
42
|
+
/.bundle/
|
43
|
+
/vendor/bundle
|
44
|
+
/lib/bundler/man/
|
45
|
+
|
46
|
+
# for a library or gem, you might want to ignore these files since the code is
|
47
|
+
# intended to run in multiple environments; otherwise, check them in:
|
48
|
+
# Gemfile.lock
|
49
|
+
# .ruby-version
|
50
|
+
# .ruby-gemset
|
51
|
+
|
52
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
53
|
+
.rvmrc
|
54
|
+
|
55
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
56
|
+
# .rubocop-https?--*
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
autometrics (0.0.1)
|
5
|
+
prometheus-client
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
bundler-audit (0.9.1)
|
11
|
+
bundler (>= 1.2.0, < 3)
|
12
|
+
thor (~> 1.0)
|
13
|
+
byebug (11.1.3)
|
14
|
+
prometheus-client (4.0.0)
|
15
|
+
thor (1.2.1)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
arm64-darwin-21
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
autometrics!
|
22
|
+
bundler (~> 2.3)
|
23
|
+
bundler-audit
|
24
|
+
byebug
|
25
|
+
|
26
|
+
BUNDLED WITH
|
27
|
+
2.4.6
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Autometrics
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# autometrics-ruby
|
2
|
+
|
3
|
+
> :warning: **Autometrics for Ruby is under active development.** We are seeking feedback from the community on the API and implementation. Please open an issue if you have any questions or feedback!
|
4
|
+
|
5
|
+
A Ruby module that makes it easy to understand the error rate, response time, and production usage of any function in your code.
|
6
|
+
|
7
|
+
Once complete, you should only have to add a one or two lines of code, and then be able to jump straight from your IDE to live Prometheus charts for each of your HTTP/RPC handlers, database methods, or any other piece of application logic.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
- ✨ `include Autometrics` exposes utilities that can instrument class methods, in order to track useful metrics for your application
|
12
|
+
- ⚡ Minimal runtime overhead
|
13
|
+
|
14
|
+
**Coming Soon**
|
15
|
+
|
16
|
+
- 💡 Writes Prometheus queries so you can understand the data generated without knowing PromQL
|
17
|
+
- 🔗 Create links to live Prometheus charts directly into each function's docstrings, via SolarGraph
|
18
|
+
|
19
|
+
- 📊 Grafana dashboard showing the performance of all instrumented functions
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Autometrics makes use of `"prometheus-client"` under the hood, which is the aptly named Ruby client for Prometheus.
|
24
|
+
|
25
|
+
For now, you simply need to add the autometrics gem to your project, `include Autometrics` in any class you wish to observe, and then set up a `/metrics` endpoint in your app that exposes the metrics to Prometheus, if one does not already exist.
|
26
|
+
|
27
|
+
### Usage inside a class
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# Include the `Autometrics` module, then call `autometrics` to enable autometrics on specific methods
|
31
|
+
|
32
|
+
class ClassWithSomeAutometrics
|
33
|
+
include Autometrics
|
34
|
+
|
35
|
+
# Option 1: Specify an allow-list of the methods to observe
|
36
|
+
autometrics only: :foo
|
37
|
+
|
38
|
+
# Option 2: Provide an exclusion-list of the methods we should not observe
|
39
|
+
autometrics skip: :bar
|
40
|
+
|
41
|
+
def foo
|
42
|
+
p "I'm getting observed!"
|
43
|
+
end
|
44
|
+
|
45
|
+
def bar
|
46
|
+
p "I am not getting observed. :("
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Include `Autometrics::On` to enable autometrics on all methods (`initialize` is excluded by default)
|
51
|
+
class ClassWithAllAutometrics
|
52
|
+
include Autometrics::On
|
53
|
+
|
54
|
+
def foo
|
55
|
+
p "This will be observed in prometheus!"
|
56
|
+
end
|
57
|
+
|
58
|
+
def bar
|
59
|
+
p "Sooøøøoo will this!"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Usage with plain-old Ruby methods
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require "autometrics"
|
68
|
+
|
69
|
+
autometrics def top_level_foo
|
70
|
+
p "I'm getting observed!"
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
## TODOs
|
75
|
+
|
76
|
+
- [ ] Provide an example of how to use Autometrics with a Rails app
|
77
|
+
- [ ] Look for other methods to exclude by default, like `initialize`. (E.g., should we exclude private methods?)
|
78
|
+
- [ ] Add tests
|
79
|
+
- [ ] Investigate ability to swap out the prometheus client, e.g., using the [`prometheus_exporter` gem](https://github.com/discourse/prometheus_exporter)
|
80
|
+
|
81
|
+
## Developing Locally
|
82
|
+
|
83
|
+
To build the Gem:
|
84
|
+
|
85
|
+
```sh
|
86
|
+
gem build autometrics.gemspec
|
87
|
+
```
|
88
|
+
|
89
|
+
For a simple smoke test, run `bundle` and `bundle exec ruby autometrics_test_quick.rb`.
|
90
|
+
|
91
|
+
To use debug logs:
|
92
|
+
|
93
|
+
```sh
|
94
|
+
LOG_LEVEL=debug bundle exec ruby autometrics_test_quick.rb
|
95
|
+
```
|
data/autometrics.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'autometrics/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'autometrics'
|
7
|
+
spec.version = Autometrics::VERSION
|
8
|
+
spec.authors = ['Brett Beutell']
|
9
|
+
spec.email = ['brett@fiberplane.com']
|
10
|
+
|
11
|
+
spec.summary =
|
12
|
+
'Ruby implmentation of autometrics library'
|
13
|
+
spec.description =
|
14
|
+
'Add developer-friendly observability to your Ruby code with minimal setup'
|
15
|
+
spec.homepage = 'https://github.com/autometrics-dev/autometrics-ruby'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files =
|
19
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(assets|test|spec|features)/})
|
21
|
+
end
|
22
|
+
# TODO - figure out what bindir does
|
23
|
+
# spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 2.3'
|
28
|
+
# spec.add_development_dependency 'rake', '~> 12.3.3'
|
29
|
+
# spec.add_development_dependency 'rspec', '~> 3.0'
|
30
|
+
|
31
|
+
# TODO - pin version
|
32
|
+
spec.add_dependency 'prometheus-client'
|
33
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'lib/autometrics'
|
2
|
+
|
3
|
+
# A class that has autometrics off by default
|
4
|
+
class ClassWithNoAutometrics
|
5
|
+
include Autometrics
|
6
|
+
|
7
|
+
# Uncomment this line to turn on autometrics for this class
|
8
|
+
# autometrics
|
9
|
+
|
10
|
+
def instance_method_of_class
|
11
|
+
p "[instance_method_of_class] You should see no [autometrics] around this function call"
|
12
|
+
end
|
13
|
+
|
14
|
+
def another_instance_method_of_class
|
15
|
+
p "[another_instance_method_of_class] You should see no [autometrics] around this function call"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class_with_none = ClassWithNoAutometrics.new
|
20
|
+
class_with_none.instance_method_of_class
|
21
|
+
class_with_none.another_instance_method_of_class
|
22
|
+
|
23
|
+
module AutometricsTest
|
24
|
+
class ClassWithSomeAutometrics
|
25
|
+
include Autometrics::On
|
26
|
+
|
27
|
+
autometrics only: :foo
|
28
|
+
|
29
|
+
def foo
|
30
|
+
p "`foo` here! You should see some [autometrics::foo] logs around me"
|
31
|
+
end
|
32
|
+
|
33
|
+
def bar
|
34
|
+
p "`bar` here! You shouldn't see any [autometrics::bar] logs by me"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class_with_some = AutometricsTest::ClassWithSomeAutometrics.new
|
40
|
+
class_with_some.foo
|
41
|
+
class_with_some.bar
|
42
|
+
|
43
|
+
|
44
|
+
autometrics def bare_function
|
45
|
+
puts "[bare_function] You should see [self.autometrics] around this function call"
|
46
|
+
end
|
47
|
+
|
48
|
+
bare_function
|
49
|
+
|
50
|
+
puts "*****"
|
51
|
+
puts "Now let's check the metrics we've collected"
|
52
|
+
puts Autometrics::PROMETHEUS.test_get_values({ function: :bare_function, module: '' })
|
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'autometrics', path: '../../'
|
4
|
+
|
5
|
+
gem 'puma'
|
6
|
+
gem 'sinatra'
|
7
|
+
gem 'yaml'
|
8
|
+
|
9
|
+
group :development, :test do
|
10
|
+
gem 'byebug', '~> 11.1'
|
11
|
+
# NOTE - Uncomment to use local solargraph gem(s)
|
12
|
+
# This is useful for testing integrated documentation functionality (still WIP)
|
13
|
+
#
|
14
|
+
# gem 'solargraph', path: '../../path/to/solargraph'
|
15
|
+
# gem 'solargraph-autometrics', path: '../../path/to/solargraph-autometrics'
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
autometrics (0.0.1)
|
5
|
+
prometheus-client
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
byebug (11.1.3)
|
11
|
+
mustermann (3.0.0)
|
12
|
+
ruby2_keywords (~> 0.0.1)
|
13
|
+
nio4r (2.5.8)
|
14
|
+
prometheus-client (4.0.0)
|
15
|
+
puma (5.6.5)
|
16
|
+
nio4r (~> 2.0)
|
17
|
+
rack (2.2.6.2)
|
18
|
+
rack-protection (3.0.5)
|
19
|
+
rack
|
20
|
+
ruby2_keywords (0.0.5)
|
21
|
+
sinatra (3.0.5)
|
22
|
+
mustermann (~> 3.0)
|
23
|
+
rack (~> 2.2, >= 2.2.4)
|
24
|
+
rack-protection (= 3.0.5)
|
25
|
+
tilt (~> 2.0)
|
26
|
+
tilt (2.1.0)
|
27
|
+
yaml (0.2.1)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
arm64-darwin-21
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
autometrics!
|
34
|
+
byebug (~> 11.1)
|
35
|
+
puma
|
36
|
+
sinatra
|
37
|
+
yaml
|
38
|
+
|
39
|
+
BUNDLED WITH
|
40
|
+
2.4.6
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Autometrics Sinatra Example
|
2
|
+
|
3
|
+
This project contains a simple Sinatra app that can be used to test the Autometrics library.
|
4
|
+
|
5
|
+
Here, we're using autometrics in the `db.rb` module, in order to generate metrics for our database calls.
|
6
|
+
|
7
|
+
## App Overview
|
8
|
+
|
9
|
+
> Example code adatped from https://guides.railsgirls.com/sinatra-app
|
10
|
+
|
11
|
+
The app itself is a JSON api that can record vote tallies. To make things concrete, we'll say we're voting on pizza toppings.
|
12
|
+
|
13
|
+
There is a `POST /cast` endpoint that accepts a `vote` parameter. The value of the `vote` parameter is the name of a pizza topping. The endpoint will increment the vote count for that topping.
|
14
|
+
|
15
|
+
There is a `GET /results` endpoint that returns a JSON object with the current vote tallies.
|
16
|
+
|
17
|
+
There is a simple database module in `db.rb` that provides methods for storing votes and retrieving vote tallies. The "database" is really just a local yaml file.
|
18
|
+
|
19
|
+
In this example, we generate metrics for our "database" calls, and expose the metrics to prometheus on the `/metrics` endpoint, which is set up in `suffragist.rb`. (See the line `use Prometheus::Middleware::Exporter`.)
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Test the API
|
24
|
+
|
25
|
+
```sh
|
26
|
+
# Install dependencies
|
27
|
+
bundle install
|
28
|
+
|
29
|
+
# Start server
|
30
|
+
bundle exec ruby suffragist.rb
|
31
|
+
|
32
|
+
# Vote for a pizza topping
|
33
|
+
curl -XPOST "http://localhost:4567/cast?vote=mushroom"
|
34
|
+
|
35
|
+
# See votes
|
36
|
+
curl localhost:4567/results
|
37
|
+
|
38
|
+
# View metrics (these would be scraped by prometheus)
|
39
|
+
curl localhost:4567/metrics
|
40
|
+
```
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'autometrics'
|
2
|
+
require 'yaml/store'
|
3
|
+
|
4
|
+
module Database
|
5
|
+
# A simple database client that stores and retrieves data from a local YAML file.
|
6
|
+
class Client
|
7
|
+
include Autometrics
|
8
|
+
|
9
|
+
autometrics only: [:save_vote, :get_votes]
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@store = YAML::Store.new 'votes.yml'
|
13
|
+
end
|
14
|
+
|
15
|
+
def save_vote(vote)
|
16
|
+
@store.transaction do
|
17
|
+
@store['votes'] ||= {}
|
18
|
+
@store['votes'][vote] ||= 0
|
19
|
+
@store['votes'][vote] += 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_votes
|
24
|
+
@store.transaction { @store['votes'] } || {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'prometheus/middleware/exporter'
|
3
|
+
|
4
|
+
require_relative 'db'
|
5
|
+
|
6
|
+
# NOTE - This creates a `/metrics` endpoint on the app that can be scraped by Prometheus
|
7
|
+
use Prometheus::Middleware::Exporter
|
8
|
+
|
9
|
+
# POST To vote for a pizza topping with query parameter `?vote=<topping>`
|
10
|
+
post '/cast' do
|
11
|
+
@vote = params['vote']
|
12
|
+
@db = Database::Client.new
|
13
|
+
@db.save_vote(@vote)
|
14
|
+
|
15
|
+
content_type :json
|
16
|
+
status 201
|
17
|
+
{ vote: @vote }.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
# GET results of all votes, result should be a hash of strings to integers
|
21
|
+
get '/results' do
|
22
|
+
@db = Database::Client.new
|
23
|
+
@votes = @db.get_votes
|
24
|
+
|
25
|
+
content_type :json
|
26
|
+
@votes.to_json
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Autometrics
|
4
|
+
module Logging
|
5
|
+
DEFAULT_LOG_LEVEL = Logger::WARN
|
6
|
+
|
7
|
+
LOG_LEVELS = {
|
8
|
+
'warn' => Logger::WARN,
|
9
|
+
'info' => Logger::INFO,
|
10
|
+
'debug' => Logger::DEBUG
|
11
|
+
}
|
12
|
+
|
13
|
+
@@logger = Logger.new(STDERR, level: ENV["LOG_LEVEL"] || DEFAULT_LOG_LEVEL)
|
14
|
+
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
15
|
+
"[#{severity}] #{msg}\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
# When you call module_function within a module, it creates copies of the specified methods as module-level methods.
|
19
|
+
module_function
|
20
|
+
|
21
|
+
# @return [Logger]
|
22
|
+
def logger
|
23
|
+
@@logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'prometheus/client'
|
3
|
+
|
4
|
+
module Autometrics
|
5
|
+
AUTOMETRICS_PROMETHEUS_REGISTRY = Prometheus::Client.registry
|
6
|
+
|
7
|
+
class PrometheusClient
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
attr_accessor :function_calls_counter, :function_calls_duration
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@function_calls_counter = Prometheus::Client::Counter.new(
|
14
|
+
:function_calls_count,
|
15
|
+
docstring: 'A counter of function calls',
|
16
|
+
labels: [:function, :module, :result]
|
17
|
+
)
|
18
|
+
AUTOMETRICS_PROMETHEUS_REGISTRY.register(@function_calls_counter)
|
19
|
+
|
20
|
+
@function_calls_duration = Prometheus::Client::Histogram.new(
|
21
|
+
:function_calls_duration,
|
22
|
+
docstring: 'A histogram of function durations',
|
23
|
+
labels: [:function, :module]
|
24
|
+
)
|
25
|
+
|
26
|
+
AUTOMETRICS_PROMETHEUS_REGISTRY.register(function_calls_duration)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_get_values(labels)
|
30
|
+
# Example labels: { function: :bare_function, module: '' }
|
31
|
+
{
|
32
|
+
function_calls_counter: function_calls_counter.get(labels: { **labels, result: "ok" }),
|
33
|
+
function_calls_duration: function_calls_duration.get(labels: labels)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/autometrics.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'prometheus/client'
|
2
|
+
|
3
|
+
require_relative 'autometrics/prometheus-client'
|
4
|
+
require_relative 'autometrics/logging'
|
5
|
+
|
6
|
+
# Module for adding autometrics functionality to a class
|
7
|
+
module Autometrics
|
8
|
+
PROMETHEUS = Autometrics::PrometheusClient.instance
|
9
|
+
|
10
|
+
# Add necessary autometrics methods and state when we're included in a class
|
11
|
+
def self.included(klass)
|
12
|
+
klass.extend(ClassMethods)
|
13
|
+
# HACK - turns off autometrics by default
|
14
|
+
klass.extend(Module.new do
|
15
|
+
def initialize(*args, &block)
|
16
|
+
super
|
17
|
+
autometrics(disabled: true)
|
18
|
+
end
|
19
|
+
end)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Module that automatically turns on autometrics when included
|
23
|
+
# INVESTIGATE - turn this into the pattern that allows you to pass arguments to the included method
|
24
|
+
module On
|
25
|
+
def self.included(klass)
|
26
|
+
klass.extend(ClassMethods)
|
27
|
+
# HACK - turns on autometrics here when user includes `On`
|
28
|
+
klass.extend(Module.new do
|
29
|
+
def initialize(*args, &block)
|
30
|
+
super
|
31
|
+
autometrics(disabled: false)
|
32
|
+
end
|
33
|
+
end)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def autometrics(**options)
|
39
|
+
# Flag to turn off autometrics for this instance
|
40
|
+
@autometrics_enabled = !options[:disabled]
|
41
|
+
|
42
|
+
# Allow-list of methods (as symbols) for which we'll gather metrics
|
43
|
+
only = options[:only]
|
44
|
+
if only
|
45
|
+
@autometrics_only = if only.is_a?(Array) then only else [only] end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Deny-list of methods (as symbols) to skip gathering autometrics for
|
49
|
+
@autometrics_skip = options[:skip] || []
|
50
|
+
# Do not gather metrics for the `initialize` method by default
|
51
|
+
@autometrics_skip_initialize = options[:skip_initialize] || true
|
52
|
+
# INVESTIGATE - add `skip_private_methods` option?
|
53
|
+
@autometrics_skip << :initialize if @autometrics_skip_initialize
|
54
|
+
|
55
|
+
# TODO - log clearer warning if `disabled` is true and other options are passed in
|
56
|
+
should_warn_about_only = !@autometrics_enabled && instance_variable_defined?(@autometrics_only)
|
57
|
+
if should_warn_about_only
|
58
|
+
Logging.logger.warn "[Autometrics] 'only' option is present, but autometrics is disabled for this class, so no metrics will be gathered for #{@autometrics_only}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Metaprogramming magic to redefine methods as they are added to the class
|
63
|
+
def method_added(method_name)
|
64
|
+
return unless @autometrics_enabled
|
65
|
+
|
66
|
+
if instance_variable_defined?(:@autometrics_skip) && @autometrics_skip.include?(method_name)
|
67
|
+
Logging.logger.debug "Skipping autometrics for #{method_name} because you told me to skip it"
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
if instance_variable_defined?(:@autometrics_only) && !@autometrics_only.include?(method_name)
|
72
|
+
Logging.logger.debug "Skipping autometrics for #{method_name} because it's not in the list of 'only' methods"
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
# HACK - Temporarily disable this flag so that `define_method` does not go into an infinite loop when we redefine the method below
|
77
|
+
@autometrics_enabled = false
|
78
|
+
|
79
|
+
# Alias the original method so we can reference it later
|
80
|
+
original_method_name = "#{method_name}_without_autometrics".to_sym
|
81
|
+
alias_method original_method_name, method_name
|
82
|
+
|
83
|
+
# Redefine the original method and wrap it with autometrics logic
|
84
|
+
# NOTE - Only the contents inside define_method's block are executed in the context of the instance.
|
85
|
+
define_method(method_name) do |*args, &fn|
|
86
|
+
prometheus_client = Autometrics::PrometheusClient.instance
|
87
|
+
get_original_result = lambda { send(original_method_name, *args, &fn) }
|
88
|
+
module_name = self.class.name
|
89
|
+
wrap_with_autometrics(get_original_result, prometheus_client, module_name, method_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# HACK - Turn our autometrics flag back on, since we disabled it above
|
93
|
+
@autometrics_enabled = true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# NOTE - I think this is the only way to make autometrics work on top-level method calls
|
100
|
+
# That is, we need to to have a top-level export like this... but I'm not a Ruby expert, so I'm not sure
|
101
|
+
#
|
102
|
+
# Usage: `autometrics def my_method; end`
|
103
|
+
def autometrics(method_name)
|
104
|
+
Autometrics::Logging.logger.debug "[self.autometrics] Adding autometrics to #{method_name}"
|
105
|
+
|
106
|
+
# Get a reference to the method that we're wrapping
|
107
|
+
original_method = method(method_name)
|
108
|
+
|
109
|
+
define_method(method_name) do |*args, &fn|
|
110
|
+
prometheus_client = Autometrics::PrometheusClient.instance
|
111
|
+
get_original_result = lambda { original_method.call(*args, &fn) }
|
112
|
+
# TODO - I'm not sure how to get/annotate the module name for a bare function call.
|
113
|
+
# Right now we're just doing an empty string.
|
114
|
+
# That said, in some code bases (like a Sinatra app?), we might consider the filename the module.
|
115
|
+
module_name = ""
|
116
|
+
|
117
|
+
wrap_with_autometrics(get_original_result, prometheus_client, module_name, method_name)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Helper method for wrapping a method with autometrics logic
|
122
|
+
# @param get_result_lambda - a lambda that returns the result of the original method
|
123
|
+
# @param prometheus_client - an instance of the Prometheus client to record metrics
|
124
|
+
# @param module_name - the name of the module that the method is defined in
|
125
|
+
# @param method_name - the name of the method being wrapped
|
126
|
+
def wrap_with_autometrics(get_result_lambda, prometheus_client, module_name, method_name)
|
127
|
+
labels = {
|
128
|
+
function: method_name,
|
129
|
+
module: module_name
|
130
|
+
}
|
131
|
+
|
132
|
+
begin
|
133
|
+
Autometrics::Logging.logger.debug "[self.wrap_with_autometrics::#{method_name}] Incrementing function calls with labels: #{labels}"
|
134
|
+
|
135
|
+
# Calculate execution time
|
136
|
+
# Use `Process.clock_gettime` instead of `Time.now` for measuring elapsed time
|
137
|
+
# See: https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
|
138
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
139
|
+
original_result = get_result_lambda.call
|
140
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
141
|
+
elapsed_time = end_time - start_time
|
142
|
+
|
143
|
+
Autometrics::Logging.logger.debug "[self.wrap_with_autometrics::#{method_name}] Observing method with labels: #{labels}"
|
144
|
+
|
145
|
+
prometheus_client.function_calls_duration.observe(elapsed_time, labels: labels)
|
146
|
+
|
147
|
+
# TODO - move to constants file
|
148
|
+
labels[:result] = "ok"
|
149
|
+
prometheus_client.function_calls_counter.increment(labels: labels)
|
150
|
+
|
151
|
+
original_result
|
152
|
+
rescue => error
|
153
|
+
# TODO - move to constants file
|
154
|
+
labels[:result] = "error"
|
155
|
+
prometheus_client.function_calls_counter.increment(labels: labels)
|
156
|
+
raise error
|
157
|
+
end
|
158
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: autometrics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brett Beutell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: prometheus-client
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Add developer-friendly observability to your Ruby code with minimal setup
|
42
|
+
email:
|
43
|
+
- brett@fiberplane.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- Gemfile
|
50
|
+
- Gemfile.lock
|
51
|
+
- LICENSE
|
52
|
+
- README.md
|
53
|
+
- autometrics.gemspec
|
54
|
+
- autometrics_test_quick.rb
|
55
|
+
- examples/autometrics-sinatra-example/.gitignore
|
56
|
+
- examples/autometrics-sinatra-example/Gemfile
|
57
|
+
- examples/autometrics-sinatra-example/Gemfile.lock
|
58
|
+
- examples/autometrics-sinatra-example/README.md
|
59
|
+
- examples/autometrics-sinatra-example/db.rb
|
60
|
+
- examples/autometrics-sinatra-example/suffragist.rb
|
61
|
+
- lib/autometrics.rb
|
62
|
+
- lib/autometrics/logging.rb
|
63
|
+
- lib/autometrics/prometheus-client.rb
|
64
|
+
- lib/autometrics/version.rb
|
65
|
+
homepage: https://github.com/autometrics-dev/autometrics-ruby
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.4.6
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Ruby implmentation of autometrics library
|
88
|
+
test_files: []
|