├── .github └── workflows │ ├── build-release.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── yabeda │ ├── rails.rb │ └── rails │ ├── config.rb │ ├── event.rb │ ├── railtie.rb │ └── version.rb ├── spec ├── spec_helper.rb ├── support │ └── rails_app.rb └── yabeda │ └── rails_spec.rb ├── yabeda-rails-logo.png └── yabeda-rails.gemspec /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release gem to RubyGems 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 # Fetch current tag as annotated. See https://github.com/actions/checkout/issues/290 15 | - uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: 2.7 18 | - name: "Extract data from tag: version, message, body" 19 | id: tag 20 | run: | 21 | git fetch --tags --force # Really fetch annotated tag. See https://github.com/actions/checkout/issues/290#issuecomment-680260080 22 | echo ::set-output name=version::${GITHUB_REF#refs/tags/v} 23 | echo ::set-output name=subject::$(git for-each-ref $GITHUB_REF --format='%(contents:subject)') 24 | BODY="$(git for-each-ref $GITHUB_REF --format='%(contents:body)')" 25 | # Extract changelog entries between this and previous version headers 26 | escaped_version=$(echo ${GITHUB_REF#refs/tags/v} | sed -e 's/[]\/$*.^[]/\\&/g') 27 | changelog=$(awk "BEGIN{inrelease=0} /## ${escaped_version}/{inrelease=1;next} /## [0-9]+\.[0-9]+\.[0-9]+/{inrelease=0;exit} {if (inrelease) print}" CHANGELOG.md) 28 | # Multiline body for release. See https://github.community/t/set-output-truncates-multiline-strings/16852/5 29 | BODY="${BODY}"$'\n'"${changelog}" 30 | BODY="${BODY//'%'/'%25'}" 31 | BODY="${BODY//$'\n'/'%0A'}" 32 | BODY="${BODY//$'\r'/'%0D'}" 33 | echo "::set-output name=body::$BODY" 34 | # Add pre-release option if tag name has any suffix after vMAJOR.MINOR.PATCH 35 | if [[ ${GITHUB_REF#refs/tags/} =~ ^v[0-9]+\.[0-9]+\.[0-9]+.+ ]]; then 36 | echo ::set-output name=prerelease::true 37 | fi 38 | - name: Build gem 39 | run: gem build 40 | - name: Calculate checksums 41 | run: sha256sum yabeda-rails-${{ steps.tag.outputs.version }}.gem > SHA256SUM 42 | - name: Check version 43 | run: ls -l yabeda-rails-${{ steps.tag.outputs.version }}.gem 44 | - name: Create Release 45 | id: create_release 46 | uses: actions/create-release@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | tag_name: ${{ github.ref }} 51 | release_name: ${{ steps.tag.outputs.subject }} 52 | body: ${{ steps.tag.outputs.body }} 53 | draft: false 54 | prerelease: ${{ steps.tag.outputs.prerelease }} 55 | - name: Upload built gem as release asset 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ steps.create_release.outputs.upload_url }} 61 | asset_path: yabeda-rails-${{ steps.tag.outputs.version }}.gem 62 | asset_name: yabeda-rails-${{ steps.tag.outputs.version }}.gem 63 | asset_content_type: application/x-tar 64 | - name: Upload checksums as release asset 65 | uses: actions/upload-release-asset@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | upload_url: ${{ steps.create_release.outputs.upload_url }} 70 | asset_path: SHA256SUM 71 | asset_name: SHA256SUM 72 | asset_content_type: text/plain 73 | - name: Publish to GitHub packages 74 | env: 75 | GEM_HOST_API_KEY: Bearer ${{ secrets.GITHUB_TOKEN }} 76 | run: | 77 | gem push yabeda-rails-${{ steps.tag.outputs.version }}.gem --host https://rubygems.pkg.github.com/${{ github.repository_owner }} 78 | - name: Publish to RubyGems 79 | env: 80 | GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_API_KEY }}" 81 | run: | 82 | gem push yabeda-rails-${{ steps.tag.outputs.version }}.gem 83 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | tags-ignore: 9 | - 'v*' 10 | 11 | jobs: 12 | rubocop: 13 | # Skip running tests for local pull requests (use push event instead), run only for foreign ones 14 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login 15 | name: RuboCop 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: "3.1" 22 | bundler-cache: true 23 | - name: Lint Ruby code with RuboCop 24 | run: | 25 | bundle exec rubocop 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | tags-ignore: 9 | - 'v*' 10 | 11 | jobs: 12 | test: 13 | name: 'Rails ${{ matrix.rails }} × Ruby ${{ matrix.ruby }}' 14 | # Skip running tests for local pull requests (use push event instead), run only for foreign ones 15 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - ruby: "3.2" 22 | rails: "HEAD" 23 | - ruby: "3.1" 24 | rails: "7.0" 25 | - ruby: "3.0" 26 | rails: "6.1" 27 | - ruby: "2.7" 28 | rails: "6.0" 29 | container: 30 | image: ruby:${{ matrix.ruby }} 31 | env: 32 | CI: true 33 | RAILS_VERSION: ${{ matrix.rails }} 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/cache@v2 37 | with: 38 | path: vendor/bundle 39 | key: bundle-${{ matrix.ruby }}-${{ hashFiles('**/*.gemspec') }}-${{ hashFiles('**/Gemfile') }} 40 | restore-keys: | 41 | bundle-${{ matrix.ruby }}-${{ hashFiles('**/*.gemspec') }}-${{ hashFiles('**/Gemfile') }} 42 | bundle-${{ matrix.ruby }}- 43 | - name: Bundle install 44 | run: | 45 | bundle config path vendor/bundle 46 | bundle install 47 | bundle update 48 | - name: Run RSpec 49 | run: bundle exec rspec 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.5 7 | 8 | Metrics/BlockLength: 9 | Exclude: 10 | - "Gemfile" 11 | - "spec/**/*" 12 | 13 | Style/StringLiterals: 14 | EnforcedStyle: double_quotes 15 | 16 | # Allow to use let! 17 | RSpec/LetSetup: 18 | Enabled: false 19 | 20 | RSpec/MultipleExpectations: 21 | Enabled: false 22 | 23 | Bundler/OrderedGems: 24 | Enabled: false 25 | 26 | Style/TrailingCommaInArguments: 27 | Description: 'Checks for trailing comma in argument lists.' 28 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma' 29 | Enabled: true 30 | EnforcedStyleForMultiline: consistent_comma 31 | 32 | Style/TrailingCommaInArrayLiteral: 33 | Description: 'Checks for trailing comma in array literals.' 34 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 35 | Enabled: true 36 | EnforcedStyleForMultiline: consistent_comma 37 | 38 | Style/TrailingCommaInHashLiteral: 39 | Description: 'Checks for trailing comma in hash literals.' 40 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 41 | Enabled: true 42 | EnforcedStyleForMultiline: consistent_comma 43 | 44 | Bundler/DuplicatedGem: 45 | Enabled: false 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.9.0 - 2023-08-03 9 | 10 | ### Added 11 | 12 | - Ability to switch controller name case in `controller` tag between `:snake` and `:camel` case. [@lewispb][] in [#26](https://github.com/yabeda-rb/yabeda-rails/pull/26) 13 | 14 | ## Changed 15 | 16 | - Minimal Ruby version increased to 2.5. [@Envek][] 17 | 18 | ## 0.8.1 - 2022-06-06 19 | 20 | ### Fixed 21 | 22 | - Fill status codes for responses with unhandled exceptions. [@dks17][] in [#24](https://github.com/yabeda-rb/yabeda-rails/pull/24) 23 | 24 | ## 0.8.0 - 2022-05-30 25 | 26 | ### Added 27 | 28 | - Add ability to expose custom Apdex target value for later use in graphs/alerts. [@Envek][] in [#18](https://github.com/yabeda-rb/yabeda-rails/pull/18) 29 | 30 | ### Changed 31 | 32 | - Reduce number of dependencies by depending only on railties instead of the whole Ruby on Rails. [@lautis][] in [#23](https://github.com/yabeda-rb/yabeda-rails/pull/23). 33 | 34 | ## 0.7.2 - 2021-03-15 35 | 36 | ### Fixed 37 | 38 | - Fix undesirable overwrite of metric tags when global `default_tag` is declared with one of tag names that are being used by yabeda-rails, like `controller`. [@liaden] in [#19](https://github.com/yabeda-rb/yabeda-rails/pull/19) 39 | 40 | ## 0.7.1 - 2020-10-02 41 | 42 | ### Changed 43 | 44 | - Explicitly require previously removed railtie to fix case when it doesn't get required in `yabeda` gem (if `yabeda` is required before `rails`). See [yabeda-rb/yabeda#15](https://github.com/yabeda-rb/yabeda/issues/15). @Envek 45 | 46 | ## 0.7.0 - 2020-08-21 47 | 48 | ### Removed 49 | 50 | - Railtie to configure Yabeda – it is moved into Yabeda itself. Increase required Yabeda version to keep behavior for users who require only `yabeda-rails` in their Gemfiles. @Envek 51 | 52 | ## 0.6.0 - 2020-08-06 53 | 54 | ### Added 55 | 56 | - Ability to add default/custom tags to metrics from controllers. @raivil in [#13](https://github.com/yabeda-rb/yabeda-rails/pull/13) 57 | 58 | ## 0.5.0 - 2020-03-27 59 | 60 | ### Added 61 | 62 | - Support for Unicorn application server. @vast in [#9](https://github.com/yabeda-rb/yabeda-rails/pull/9) 63 | 64 | ## 0.4.0 - 2020-01-28 65 | 66 | ### Changed 67 | 68 | - Configure Yabeda after application initialization as since 0.4.0 Yabeda requires to call configuration logic explicitly. @Envek 69 | 70 | ## 0.2.0 - 2020-01-14 71 | 72 | ### Changed 73 | 74 | - Added `tags` option to metric declarations for compatibility with yabeda and yabeda-prometheus 0.2. @Envek 75 | 76 | ## 0.1.2 - 2019-01-19 77 | 78 | ### Added 79 | 80 | - Support for Puma application server. @daffydowden 81 | 82 | ## 0.1.1 - 2018-10-17 83 | 84 | ### Changed 85 | 86 | - Renamed evil-metrics-rails gem to yabeda-rails. @Envek 87 | 88 | ## 0.1.0 - 2018-10-03 89 | 90 | - Initial release of evil-metrics-rails gem. @Envek 91 | 92 | Basic metrics for request durations by controller, action, status, format, and method. ActiveRecord and ActionView timings. 93 | 94 | [@Envek]: https://github.com/Envek "Andrey Novikov" 95 | [@liaden]: https://github.com/liaden "Joel Johnson" 96 | [@lautis]: https://github.com/lautis "Ville Lautanala" 97 | [@dks17]: https://github.com/dks17 "Konstantin" 98 | [@lewispb]: https://github.com/lewispb "Lewis Buckley" 99 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in yabeda-rails.gemspec 8 | gemspec 9 | 10 | rails_version = ENV.fetch("RAILS_VERSION", "~> 7.0") 11 | case rails_version 12 | when "HEAD" 13 | git "https://github.com/rails/rails.git" do 14 | gem "rails" 15 | gem "activesupport" 16 | gem "railties" 17 | end 18 | else 19 | rails_version = "~> #{rails_version}.0" if rails_version.match?(/^\d+\.\d+$/) 20 | gem "rails", rails_version 21 | gem "activesupport", rails_version 22 | gem "railties", rails_version 23 | end 24 | 25 | group :development, :test do 26 | gem "yabeda", "~> 0.11" # Test helpers 27 | gem "rspec-rails" 28 | 29 | gem "debug" 30 | 31 | gem "rubocop", "~> 1.8" 32 | gem "rubocop-rspec" 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andrey Novikov 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Yabeda::Rails](./yabeda-rails-logo.png) 2 | 3 | Built-in metrics for out-of-the box [Rails] applications monitoring. 4 | 5 | If your monitoring system already collects Rails metrics (e.g. NewRelic) then most probably you don't need this gem. 6 | 7 | Sample Grafana dashboard ID: [11668](https://grafana.com/grafana/dashboards/11668) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'yabeda-rails' 15 | # Then add monitoring system adapter, e.g.: 16 | # gem 'yabeda-prometheus' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | ### Registering metrics on server process start 24 | 25 | Currently, yabeda-rails automatically registers rails metrics when a server is started via `rails server`, `puma -C config/puma.rb` or `unicorn -c`. However, other application servers or launching via `rackup` aren't supported at the moment. 26 | 27 | A possible workaround is to detect server process and manually activate yabeda-rails in an initializer: 28 | 29 | ```ruby 30 | # config/initializers/yabeda.rb 31 | 32 | if your_app_server_process? # Your logic here 33 | Yabeda::Rails.install! 34 | end 35 | ``` 36 | 37 | You always can add support for your app server to [lib/yabeda/rails/railtie.rb](lib/yabeda/rails/railtie.rb). Pull Requests are always welcome! 38 | 39 | 40 | ## Metrics 41 | 42 | - Total web requests received: `rails_requests_total` 43 | - Web request duration: `rails_request_duration` (in seconds) 44 | - Views rendering duration: `rails_view_runtime` (in seconds) 45 | - DB request duration: `rails_db_runtime` (in seconds) 46 | 47 | 48 | ## Hooks 49 | 50 | - `on_controller_action`: Allows to collect 51 | 52 | ```ruby 53 | Yabeda::Rails.on_controller_action do |event, labels| 54 | next unless event.payload[:ext_service_runtime] 55 | time_in_seconds = event.payload[:ext_service_runtime] / 1000.0 56 | rails_ext_service_runtime.measure(labels, time_in_seconds) 57 | end 58 | ``` 59 | 60 | ## Custom tags 61 | 62 | You can add additional tags to the existing metrics by adding custom payload to your controller. 63 | 64 | ```ruby 65 | # This block is optional but some adapters (like Prometheus) requires that all tags should be declared in advance 66 | Yabeda.configure do 67 | default_tag :importance, nil 68 | end 69 | 70 | class ApplicationController < ActionController::Base 71 | def append_info_to_payload(payload) 72 | super 73 | payload[:importance] = extract_importance(params) 74 | end 75 | end 76 | ``` 77 | `append_info_to_payload` is a method from [ActionController::Instrumentation](https://api.rubyonrails.org/classes/ActionController/Instrumentation.html#method-i-append_info_to_payload) 78 | 79 | 80 | ## Development 81 | 82 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 83 | 84 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 85 | 86 | ### Releasing 87 | 88 | 1. Bump version number in `lib/yabeda/rails/version.rb` 89 | 90 | In case of pre-releases keep in mind [rubygems/rubygems#3086](https://github.com/rubygems/rubygems/issues/3086) and check version with command like `Gem::Version.new(Yabeda::Rails::VERSION).to_s` 91 | 92 | 2. Fill `CHANGELOG.md` with missing changes, add header with version and date. 93 | 94 | 3. Make a commit: 95 | 96 | ```sh 97 | git add lib/yabeda/rails/version.rb CHANGELOG.md 98 | version=$(ruby -r ./lib/yabeda/rails/version.rb -e "puts Gem::Version.new(Yabeda::Rails::VERSION)") 99 | git commit --message="${version}: " --edit 100 | ``` 101 | 102 | 4. Create annotated tag: 103 | 104 | ```sh 105 | git tag v${version} --annotate --message="${version}: " --edit --sign 106 | ``` 107 | 108 | 5. Fill version name into subject line and (optionally) some description (list of changes will be taken from changelog and appended automatically) 109 | 110 | 6. Push it: 111 | 112 | ```sh 113 | git push --follow-tags 114 | ``` 115 | 116 | 7. You're done! 117 | 118 | ## Contributing 119 | 120 | Bug reports and pull requests are welcome on GitHub at https://github.com/yabeda-rb/yabeda-rails. 121 | 122 | ## License 123 | 124 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 125 | 126 | [Rails]: https://rubyonrails.org "Ruby on Rails MVC web-application framework optimized for programmer happiness" 127 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "yabeda/rails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "pry" 11 | Pry.start 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/yabeda/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yabeda" 4 | require "active_support" 5 | require "rails/railtie" 6 | require "yabeda/rails/railtie" 7 | require "yabeda/rails/config" 8 | require "yabeda/rails/event" 9 | 10 | module Yabeda 11 | # Minimal set of Rails-specific metrics for using with Yabeda 12 | module Rails 13 | LONG_RUNNING_REQUEST_BUCKETS = [ 14 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, # standard 15 | 30, 60, 120, 300, 600, # Sometimes requests may be really long-running 16 | ].freeze 17 | 18 | class << self 19 | def controller_handlers 20 | @controller_handlers ||= [] 21 | end 22 | 23 | def on_controller_action(&block) 24 | controller_handlers << block 25 | end 26 | 27 | # Declare metrics and install event handlers for collecting themya 28 | # rubocop: disable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize 29 | def install! 30 | Yabeda.configure do 31 | config = ::Yabeda::Rails.config 32 | 33 | group :rails 34 | 35 | counter :requests_total, comment: "A counter of the total number of HTTP requests rails processed.", 36 | tags: %i[controller action status format method] 37 | 38 | histogram :request_duration, tags: %i[controller action status format method], 39 | unit: :seconds, 40 | buckets: LONG_RUNNING_REQUEST_BUCKETS, 41 | comment: "A histogram of the response latency." 42 | 43 | histogram :view_runtime, unit: :seconds, buckets: LONG_RUNNING_REQUEST_BUCKETS, 44 | comment: "A histogram of the view rendering time.", 45 | tags: %i[controller action status format method] 46 | 47 | histogram :db_runtime, unit: :seconds, buckets: LONG_RUNNING_REQUEST_BUCKETS, 48 | comment: "A histogram of the activerecord execution time.", 49 | tags: %i[controller action status format method] 50 | 51 | if config.apdex_target 52 | gauge :apdex_target, unit: :seconds, 53 | comment: "Tolerable time for Apdex (T value: maximum duration of satisfactory request)" 54 | collect { rails_apdex_target.set({}, config.apdex_target) } 55 | end 56 | 57 | ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| 58 | event = Yabeda::Rails::Event.new(*args) 59 | 60 | rails_requests_total.increment(event.labels) 61 | rails_request_duration.measure(event.labels, event.duration) 62 | rails_view_runtime.measure(event.labels, event.view_runtime) 63 | rails_db_runtime.measure(event.labels, event.db_runtime) 64 | 65 | Yabeda::Rails.controller_handlers.each do |handler| 66 | handler.call(event, event.labels) 67 | end 68 | end 69 | end 70 | end 71 | # rubocop: enable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize 72 | 73 | def config 74 | @config ||= Config.new 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/yabeda/rails/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "anyway" 4 | 5 | module Yabeda 6 | module Rails 7 | # yabeda-rails configuration 8 | class Config < ::Anyway::Config 9 | config_name :yabeda_rails 10 | 11 | attr_config :apdex_target 12 | attr_config controller_name_case: :snake 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/yabeda/rails/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Yabeda 4 | module Rails 5 | # ActiveSupport Event with added logic for Yabeda tags formatting 6 | class Event < ActiveSupport::Notifications::Event 7 | def labels 8 | @labels ||= begin 9 | labels = { 10 | controller: controller, 11 | action: action, 12 | status: status, 13 | format: format, 14 | method: method, 15 | } 16 | labels.merge(payload.slice(*Yabeda.default_tags.keys - labels.keys)) 17 | end 18 | end 19 | 20 | def duration 21 | ms2s super 22 | end 23 | 24 | def view_runtime 25 | ms2s payload[:view_runtime] 26 | end 27 | 28 | def db_runtime 29 | ms2s payload[:db_runtime] 30 | end 31 | 32 | private 33 | 34 | def controller 35 | case Yabeda::Rails.config.controller_name_case 36 | when :camel 37 | payload[:controller] 38 | else 39 | payload[:params]["controller"] 40 | end 41 | end 42 | 43 | def action 44 | payload[:action] 45 | end 46 | 47 | def status 48 | if payload[:status].nil? && payload[:exception].present? 49 | ActionDispatch::ExceptionWrapper.status_code_for_exception(payload[:exception].first) 50 | else 51 | payload[:status] 52 | end 53 | end 54 | 55 | def format 56 | payload[:format] 57 | end 58 | 59 | def method 60 | payload[:method].downcase 61 | end 62 | 63 | def ms2s(milliseconds) 64 | (milliseconds.to_f / 1000).round(3) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/yabeda/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Explicitly require yabeda's railtie in case if its require was skipped there. 4 | # See https://github.com/yabeda-rb/yabeda/issues/15 5 | require "yabeda/railtie" 6 | 7 | module Yabeda 8 | module Rails 9 | class Railtie < ::Rails::Railtie # :nodoc: 10 | def rails_server? 11 | ::Rails.const_defined?(:Server) 12 | end 13 | 14 | def puma_server? 15 | ::Rails.const_defined?("Puma::CLI") 16 | end 17 | 18 | def unicorn_server? 19 | ::Rails.const_defined?("Unicorn::Launcher") 20 | end 21 | 22 | initializer "yabeda-rails.metrics" do 23 | ::Yabeda::Rails.install! if rails_server? || puma_server? || unicorn_server? 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/yabeda/rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Yabeda 4 | module Rails 5 | VERSION = "0.9.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] = "test" 4 | 5 | require "bundler/setup" 6 | require "debug" 7 | require "yabeda/rails" 8 | require "yabeda/rspec" 9 | 10 | require_relative "support/rails_app" 11 | 12 | require "rspec/rails" 13 | 14 | RSpec.configure do |config| 15 | # Enable flags like --only-failures and --next-failure 16 | config.example_status_persistence_file_path = ".rspec_status" 17 | 18 | # Disable RSpec exposing methods globally on `Module` and `main` 19 | config.disable_monkey_patching! 20 | 21 | config.expect_with :rspec do |c| 22 | c.syntax = :expect 23 | end 24 | 25 | Kernel.srand config.seed 26 | config.order = :random 27 | 28 | config.before(:suite) do 29 | Yabeda::Rails.install! 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/rails_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | require "action_controller/railtie" 5 | require "active_support/railtie" 6 | 7 | class TestApplication < Rails::Application 8 | config.logger = Logger.new($stdout) 9 | config.log_level = :fatal 10 | config.consider_all_requests_local = true 11 | config.eager_load = true 12 | 13 | routes.append do 14 | get "/hello/world" => "hello#world" 15 | get "/hello/long" => "hello#long" 16 | get "/hello/internal_server_error" => "hello#internal_server_error" 17 | end 18 | end 19 | 20 | class HelloController < ActionController::API 21 | def world 22 | render json: { hello: :world } 23 | end 24 | 25 | def long 26 | sleep(0.01) 27 | render json: { good: :morning } 28 | end 29 | 30 | def internal_server_error 31 | raise StandardError 32 | end 33 | end 34 | 35 | TestApplication.initialize! 36 | -------------------------------------------------------------------------------- /spec/yabeda/rails_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "action_controller/test_case" 4 | 5 | RSpec.describe Yabeda::Rails, type: :integration do 6 | include ActionDispatch::Integration::Runner 7 | include ActionDispatch::IntegrationTest::Behavior 8 | 9 | def app 10 | TestApplication 11 | end 12 | 13 | it "increments counters for every request" do 14 | expect { get "/hello/world" }.to \ 15 | increment_yabeda_counter(Yabeda.rails.requests_total) 16 | .with_tags(controller: "hello", action: "world", status: 200, method: "get", format: :html) 17 | .by(1) 18 | end 19 | 20 | it "measure action runtime for every request" do 21 | expect { get "/hello/long" }.to \ 22 | measure_yabeda_histogram(Yabeda.rails.request_duration) 23 | .with_tags(controller: "hello", action: "long", status: 200, method: "get", format: :html) 24 | .with(be_between(0.005, 0.05)) 25 | end 26 | 27 | it "returns internal_server_error status code" do 28 | expect { get "/hello/internal_server_error" }.to \ 29 | increment_yabeda_counter(Yabeda.rails.requests_total) 30 | .with_tags(controller: "hello", action: "internal_server_error", status: 500, method: "get", format: :html) 31 | end 32 | 33 | context "with changed controller name case config tp camel case" do 34 | around do |example| 35 | original_case = described_class.config.controller_name_case 36 | described_class.config.controller_name_case = :camel 37 | example.call 38 | ensure 39 | described_class.config.controller_name_case = original_case 40 | end 41 | 42 | it "reports controller tag in camel case" do 43 | expect { get "/hello/world" }.to \ 44 | increment_yabeda_counter(Yabeda.rails.requests_total) 45 | .with_tags(controller: "HelloController", action: "world", status: 200, method: "get", format: :html) 46 | .by(1) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /yabeda-rails-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yabeda-rb/yabeda-rails/3874b3c6a938ee2451a9e4975f7839931708f7af/yabeda-rails-logo.png -------------------------------------------------------------------------------- /yabeda-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "yabeda/rails/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "yabeda-rails" 9 | spec.version = Yabeda::Rails::VERSION 10 | spec.authors = ["Andrey Novikov"] 11 | spec.email = ["envek@envek.name"] 12 | 13 | spec.summary = "Extensible metrics for monitoring Ruby on Rails application" 14 | spec.description = "Easy collecting your Rails apps metrics" 15 | spec.homepage = "https://github.com/yabeda-rb/yabeda-rails" 16 | spec.license = "MIT" 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | spec.bindir = "exe" 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ["lib"] 24 | 25 | spec.required_ruby_version = ">= 2.5" 26 | 27 | spec.add_dependency "activesupport" 28 | spec.add_dependency "anyway_config", ">= 1.3", "< 3" 29 | spec.add_dependency "railties" 30 | spec.add_dependency "yabeda", "~> 0.8" 31 | 32 | spec.add_development_dependency "bundler", "~> 2.0" 33 | spec.add_development_dependency "rake", "~> 13.0" 34 | spec.add_development_dependency "rspec", "~> 3.0" 35 | end 36 | --------------------------------------------------------------------------------