autometrics 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ group :development, :test do
6
+ gem 'byebug'
7
+ gem 'bundler-audit'
8
+ end
9
+
10
+ # Specify gem's dependencies in autometrics.gemspec
11
+ gemspec
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
+ ```
@@ -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,2 @@
1
+ # Project specific
2
+ votes.yml
@@ -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
@@ -0,0 +1,3 @@
1
+ module Autometrics
2
+ VERSION = '0.0.1'
3
+ end
@@ -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: []