autometrics 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|