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 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: []