├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── autometrics.gemspec ├── autometrics_test_quick.rb ├── examples └── autometrics-sinatra-example │ ├── .gitignore │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── db.rb │ └── suffragist.rb └── lib ├── autometrics.rb └── autometrics ├── logging.rb ├── prometheus-client.rb └── version.rb /.gitignore: -------------------------------------------------------------------------------- 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?--* 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 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 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autometrics-ruby 2 | 3 | > :warning: **Autometrics for Ruby needs community input.** 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 gem that makes it easy to understand the error rate, response time, and production usage of any function in your code. 6 | 7 | Once we complete all our `TODOs`, 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 (`gem install autometrics`), `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. There is an example Sinatra app in this repo to show how you might do this. 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 | ``` 96 | -------------------------------------------------------------------------------- /autometrics.gemspec: -------------------------------------------------------------------------------- 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 34 | -------------------------------------------------------------------------------- /autometrics_test_quick.rb: -------------------------------------------------------------------------------- 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: '' }) -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific 2 | votes.yml 3 | -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/Gemfile: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/Gemfile.lock: -------------------------------------------------------------------------------- 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 41 | -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/README.md: -------------------------------------------------------------------------------- 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 | ``` 41 | -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/db.rb: -------------------------------------------------------------------------------- 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 28 | -------------------------------------------------------------------------------- /examples/autometrics-sinatra-example/suffragist.rb: -------------------------------------------------------------------------------- 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=` 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 -------------------------------------------------------------------------------- /lib/autometrics.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/autometrics/logging.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /lib/autometrics/prometheus-client.rb: -------------------------------------------------------------------------------- 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 38 | -------------------------------------------------------------------------------- /lib/autometrics/version.rb: -------------------------------------------------------------------------------- 1 | module Autometrics 2 | VERSION = '0.0.1' 3 | end 4 | --------------------------------------------------------------------------------