├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── rails_5.2.gemfile ├── rails_6.0.gemfile ├── rails_6.1.gemfile ├── rails_7.0.gemfile ├── rails_7.1.gemfile └── rails_edge.gemfile ├── lib ├── lograge.rb └── lograge │ ├── formatters │ ├── cee.rb │ ├── graylog2.rb │ ├── helpers │ │ └── method_and_path.rb │ ├── json.rb │ ├── key_value.rb │ ├── key_value_deep.rb │ ├── l2met.rb │ ├── lines.rb │ ├── logstash.rb │ ├── ltsv.rb │ └── raw.rb │ ├── log_subscribers │ ├── action_cable.rb │ ├── action_controller.rb │ └── base.rb │ ├── ordered_options.rb │ ├── rails_ext │ ├── action_cable │ │ ├── channel │ │ │ └── base.rb │ │ ├── connection │ │ │ └── base.rb │ │ └── server │ │ │ └── base.rb │ └── rack │ │ └── logger.rb │ ├── railtie.rb │ ├── silent_logger.rb │ └── version.rb ├── lograge.gemspec ├── spec ├── formatters │ ├── cee_spec.rb │ ├── graylog2_spec.rb │ ├── helpers │ │ └── method_and_path_spec.rb │ ├── json_spec.rb │ ├── key_value_deep_spec.rb │ ├── key_value_spec.rb │ ├── l2met_spec.rb │ ├── lines_spec.rb │ ├── logstash_spec.rb │ ├── ltsv_spec.rb │ └── raw_spec.rb ├── log_subscribers │ ├── action_cable_spec.rb │ └── action_controller_spec.rb ├── lograge_spec.rb ├── silent_logger_spec.rb ├── spec_helper.rb └── support │ └── examples.rb └── tools └── console /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Docs for this file can be found here: 2 | # https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository 3 | 4 | github: ["iloveitaly"] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | 11 | # Maintain dependencies for Ruby's Bundler 12 | - package-ecosystem: "bundler" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.gemfile == 'gemfiles/rails_edge.gemfile' }} 13 | 14 | strategy: 15 | matrix: 16 | ruby: 17 | # MRI 18 | - head 19 | - "2.7" 20 | - "3.0" 21 | - "3.1" 22 | - "3.2" 23 | - "3.3" 24 | 25 | # JRuby 26 | - jruby-9.3 27 | - jruby-9.4 28 | 29 | # TruffleRuby 30 | - truffleruby-23.0 31 | 32 | gemfile: 33 | - Gemfile 34 | - gemfiles/rails_edge.gemfile # 7.2.0.alpha 35 | - gemfiles/rails_7.1.gemfile 36 | - gemfiles/rails_7.0.gemfile 37 | - gemfiles/rails_6.1.gemfile 38 | - gemfiles/rails_6.0.gemfile 39 | - gemfiles/rails_5.2.gemfile 40 | 41 | # Include additional ruby/gemfile combinations: 42 | include: 43 | # NOTE(ivy): Rails 7 requires Ruby version >= 2.7 44 | - ruby: "2.6" 45 | gemfile: Gemfile 46 | - ruby: "2.6" 47 | gemfile: gemfiles/rails_6.0.gemfile 48 | - ruby: "2.6" 49 | gemfile: gemfiles/rails_5.2.gemfile 50 | exclude: 51 | # NOTE(ivy): Rails 7 requires Ruby version >= 2.7 52 | - ruby: jruby-9.3 53 | gemfile: gemfiles/rails_edge.gemfile 54 | - ruby: jruby-9.3 55 | gemfile: gemfiles/rails_7.1.gemfile 56 | - ruby: jruby-9.3 57 | gemfile: gemfiles/rails_7.0.gemfile 58 | - ruby: jruby-9.2 59 | gemfile: gemfiles/rails_edge.gemfile 60 | - ruby: jruby-9.2 61 | gemfile: gemfiles/rails_7.1.gemfile 62 | - ruby: jruby-9.2 63 | gemfile: gemfiles/rails_7.0.gemfile 64 | # NOTE: Rails edge requires Ruby version >= 3.1 65 | - ruby: "2.7" 66 | gemfile: gemfiles/rails_edge.gemfile 67 | - ruby: "3.0" 68 | gemfile: gemfiles/rails_edge.gemfile 69 | 70 | 71 | env: 72 | BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} 73 | 74 | steps: 75 | - uses: actions/checkout@v4 76 | 77 | - uses: ruby/setup-ruby@v1 78 | with: 79 | ruby-version: ${{ matrix.ruby }} 80 | bundler-cache: true 81 | 82 | - run: bundle exec rake 83 | - run: bundle exec rubocop 84 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '21 23 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | coverage 5 | gemfiles/*.lock 6 | pkg/* 7 | vendor/bundle -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - Guardfile 4 | - vendor/**/* 5 | NewCops: enable 6 | TargetRubyVersion: 2.5 7 | 8 | Layout/LineLength: 9 | Max: 120 10 | 11 | Lint/AmbiguousBlockAssociation: 12 | Exclude: 13 | - spec/**/*_spec.rb 14 | 15 | Metrics/AbcSize: 16 | Enabled: false 17 | 18 | Metrics/BlockLength: 19 | Exclude: 20 | - spec/**/*_spec.rb 21 | 22 | Style/BlockDelimiters: 23 | Enabled: false 24 | 25 | Style/Documentation: 26 | Enabled: false 27 | 28 | Gemspec/DevelopmentDependencies: 29 | Enabled: false 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### Unreleased 4 | 5 | ### 0.14.0 6 | 7 | * Add Rails 7.1 dedicated `ActiveSupport::Deprecation` [#365](https://github.com/roidrage/lograge/pull/365) 8 | 9 | ### 0.13.0 10 | 11 | * Add Rails 6 memory allocations to default log [#355](https://github.com/roidrage/lograge/pull/355) 12 | 13 | ### 0.12.0 14 | 15 | * Preserve original Action Cable functionality by using `prepend` instead of redefining methods [#310](https://github.com/roidrage/lograge/pull/310) 16 | * Return a `Rack::BodyProxy` from the `Rails::Rack::Logger` monkey patch, this ensures the same return type as Rails [#333](https://github.com/roidrage/lograge/pull/333) 17 | 18 | * Add a new formatter `Lograge::Formatters::KeyValueDeep.new` to log object with nested key. [#282](https://github.com/roidrage/lograge/pull/282/files) 19 | 20 | ### 0.11.2 21 | 22 | * Resolve a bug with Action Cable registration [#286](https://github.com/roidrage/lograge/pull/286) 23 | 24 | ### 0.11.1 25 | 26 | * Resolve a bug with Action Cable registration [#289](https://github.com/roidrage/lograge/pull/289) 27 | 28 | ### 0.11.0 29 | 30 | * Add support for Action Cable [#257](https://github.com/roidrage/lograge/pull/257) 31 | 32 | ### 0.10.0 33 | 34 | * Strip querystring from `Location` header [#241](https://github.com/roidrage/lograge/pull/241) 35 | 36 | ### 0.9.0 37 | 38 | * Relax Rails gem dependency [#235](https://github.com/roidrage/lograge/pull/235) 39 | 40 | ### 0.8.0 41 | 42 | * Configure multiple base controllers [#230](https://github.com/roidrage/lograge/pull/230) 43 | 44 | ### 0.7.1 45 | 46 | * Bug fix for configurable controllers [#228](https://github.com/roidrage/lograge/pull/228) 47 | 48 | ### 0.7.0 49 | 50 | * Configurable base class [#227](https://github.com/roidrage/lograge/pull/227) 51 | 52 | ### 0.6.0 53 | 54 | * Replace thread-locals with `request_store` [#218](https://github.com/roidrage/lograge/pull/218) 55 | * An alternative to the `append_info_to_payload` strategy [#135](https://github.com/roidrage/lograge/pull/135) 56 | 57 | ### 0.5.1 58 | 59 | * Loosen Rails gem dependency [#209](https://github.com/roidrage/lograge/pull/209) 60 | 61 | ### 0.5.0 62 | 63 | * Rails 5.1 support [#208](https://github.com/roidrage/lograge/pull/208) 64 | 65 | ### 0.5.0.rc2 66 | 67 | * Rails 5.1 RC2 support [#207](https://github.com/roidrage/lograge/pull/207) 68 | 69 | ### 0.5.0.rc1 70 | 71 | * Rails 5.1 RC1 support [#205](https://github.com/roidrage/lograge/pull/205) 72 | 73 | ### 0.4.1 74 | 75 | * Controller name is specified by class [#184](https://github.com/roidrage/lograge/pull/184) 76 | * Loosen gemspec dependency on Rails 5 [#182](https://github.com/roidrage/lograge/pull/182) 77 | 78 | ### 0.4.0 79 | 80 | * Rails 5 support [#181](https://github.com/roidrage/lograge/pull/181) 81 | 82 | ### 0.4.0.rc2 83 | 84 | * Rails 5 rc2 support 85 | 86 | ### 0.4.0.rc1 87 | 88 | * Rails 5 rc1 support [#175](https://github.com/roidrage/lograge/pull/175) 89 | 90 | ### 0.4.0.pre4 91 | 92 | * Rails 5 beta 4 support [#174](https://github.com/roidrage/lograge/pull/174) 93 | * Retrieve controller/action from payload not nested params [cd2dc08](https://github.com/roidrage/lograge/commit/cd2dc08) 94 | 95 | ### 0.4.0.pre2 96 | 97 | * Add support for Rails 5 beta 3 [#169](https://github.com/roidrage/lograge/pull/169) 98 | 99 | ### 0.4.0.pre 100 | 101 | * Add support for Rails 5 beta 2 [#166](https://github.com/roidrage/lograge/pull/166) 102 | * End support for Ruby 1.9.3 and Rails 3 [#164](https://github.com/roidrage/lograge/pull/164) 103 | 104 | ### 0.3.6 105 | 106 | * Fix an issue with LTSV formatter [#162](https://github.com/roidrage/lograge/pull/162) 107 | 108 | ### 0.3.5 109 | 110 | * Support logging of unpermitted parameters in Rails 4+ [#154](https://github.com/roidrage/lograge/pull/154) 111 | 112 | ### 0.3.4 113 | 114 | * Added LTSV formatter () [#138](https://github.com/roidrage/lograge/pull/138) 115 | 116 | ### 0.3.3 117 | 118 | * Resolves #126 issues with status codes [#134](https://github.com/roidrage/lograge/pull/134) 119 | * Resolves build failures under rails 3.2 caused by `logstash-event` dependency 120 | * Delay loading so `config.enabled=` works from `config/initializers/*.rb` () [#62](https://github.com/roidrage/lograge/pull/62) 121 | 122 | ## 0.3.2 123 | 124 | ### Fixed 125 | * Make sure rack_cache[:verbose] can be set [#103](https://github.com/roidrage/lograge/pull/103) 126 | * Follow hash syntax for logstash-event v1.4.x [#75](https://github.com/roidrage/lograge/pull/75) 127 | * Log RecordNotFound as 404 [#27](https://github.com/roidrage/lograge/pull/27), [#110](https://github.com/roidrage/lograge/pull/110), [#112](https://github.com/roidrage/lograge/pull/112) 128 | 129 | ### Other 130 | * Use https in Gemfile #104 131 | 132 | ## 0.3.1 133 | 134 | ### Fixed 2015-01-17 135 | 136 | * Make rubocop pass 137 | 138 | ### Added 139 | 140 | * Add formatter for lines () [#35](https://github.com/roidrage/lograge/pull/35) 141 | * Rubocop and rake ci task 142 | * LICENSE.txt 143 | 144 | ### Other 145 | 146 | * Performance optimizations () [#9](https://github.com/roidrage/lograge/pull/9) 147 | * Add documentation on how to enable param logging [#68](https://github.com/roidrage/lograge/pull/68) 148 | * Add missing JSON formatter to README [#77](https://github.com/roidrage/lograge/pull/77) 149 | * Cleaning up gemspec 150 | 151 | ## 0.3.0 - 2014-03-11 152 | 153 | ### Added 154 | * Add formatter for l2met () [#47](https://github.com/roidrage/lograge/pull/47) 155 | * Add JSON formatter () [#56](https://github.com/roidrage/lograge/pull/56) 156 | * Add `before_format` hook () [#59](https://github.com/roidrage/lograge/pull/59) 157 | * Add Ruby 2.1.0 for testing on Travis CI () [#60](https://github.com/roidrage/lograge/pull/60) 158 | 159 | ### Fixed 160 | * Update Logstash formatter for Logstash 1.2 format () [#55](https://github.com/roidrage/lograge/pull/55) 161 | 162 | 163 | 164 | ## Older Versions: 165 | 166 | ### Added 167 | * Add support for Graylog2 events (Lennart Koopmann, http://github.com/lennartkoopmann) 168 | * Add support for Logstash events (Holger Just, http://github.com/meineerde) 169 | * Add `custom_options` to allow adding custom key-value pairs at runtime (Adam Cooper, https://github.com/adamcooper) 170 | 171 | ### Fixed 172 | * Fix for Rails 3.2.9 173 | * Use keys everywhere (Curt Michols, http://github.com/asenchi) 174 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing To lograge 2 | 3 | We want to start off by saying thank you 4 | for using and contributing to lograge. This project is a labor of 5 | love, and we appreciate all of the users that catch bugs, make 6 | performance improvements, and help with documentation. Every 7 | contribution is meaningful, so thank you for participating. That being 8 | said, here are a few guidelines that we ask you to follow so we can 9 | successfully address your issue. 10 | 11 | 12 | ### Submitting Issues 13 | 14 | Please include the following: 15 | 16 | * Lograge version/tag/commit hash you are using 17 | * The Rails version 18 | * Which RVM/rbenv/chruby/etc version you are using if so 19 | * The Ruby version your are using 20 | * The Operating System 21 | * A stacktrace or log output if available 22 | 23 | Describe your issue and give as much steps as necessary to reproduce 24 | it. If possible add what you expected to happen and what actually 25 | happened to get a better understanding of the problem. 26 | 27 | ### Submitting A Pull Request 28 | 29 | If you want to submit a pull request make sure your code is cleaned up 30 | and no artifacts are left behind. Make sure your commits are clearly 31 | structured and follow _the seven rules of a great commit message_: 32 | 33 | * Separate subject from body with a blank line 34 | * Limit the subject line to 50 characters 35 | * Capitalize the subject line 36 | * Do not end the subject line with a period 37 | * Use the imperative mood in the subject line 38 | * Wrap the body at 72 characters 39 | * Use the body to explain what and why vs. how 40 | 41 | (Taken from [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/)) 42 | 43 | Make sure `rake ci` passes and that you have *added* tests for your 44 | change. 45 | 46 | *Thank you.* 47 | 48 | This document is inspired by the [Rubinius](https://raw.githubusercontent.com/rubinius/rubinius/master/CONTRIBUTING.md) project. 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec 7 | 8 | gem 'pry', group: :development 9 | 10 | group :test do 11 | gem 'actionpack', '~> 6' 12 | gem 'activerecord', '~> 6' 13 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 14 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 15 | # we test against the correct code. 16 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 17 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 18 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 19 | gem 'lines' 20 | gem 'thread_safe' 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Mathias Meyer 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 | [![CI](https://github.com/roidrage/lograge/actions/workflows/ci.yml/badge.svg)](https://github.com/roidrage/lograge/actions/workflows/ci.yml) 2 | [![Gem Version](https://badge.fury.io/rb/lograge.svg)](http://badge.fury.io/rb/lograge) 3 | 4 | # Lograge - Taming Rails' Default Request Logging # 5 | 6 | Lograge is an attempt to bring sanity to Rails' noisy and unusable, unparsable 7 | and, in the context of running multiple processes and servers, unreadable 8 | default logging output. Rails' default approach to log everything is great 9 | during development, it's terrible when running it in production. It pretty much 10 | renders Rails logs useless to me. 11 | 12 | Lograge is a work in progress. I appreciate constructive feedback and criticism. 13 | My main goal is to improve Rails' logging and to show people that they don't 14 | need to stick with its defaults anymore if they don't want to. 15 | 16 | Instead of trying solving the problem of having multiple lines per request by 17 | switching Rails' logger for something that outputs syslog lines or adds a 18 | request token, Lograge replaces Rails' request logging entirely, reducing the 19 | output per request to a single line with all the important information, removing 20 | all that clutter Rails likes to include and that gets mingled up so nicely when 21 | multiple processes dump their output into a single file. 22 | 23 | Instead of having an unparsable amount of logging output like this: 24 | 25 | ``` 26 | Started GET "/" for 127.0.0.1 at 2012-03-10 14:28:14 +0100 27 | Processing by HomeController#index as HTML 28 | Rendered text template within layouts/application (0.0ms) 29 | Rendered layouts/_assets.html.erb (2.0ms) 30 | Rendered layouts/_top.html.erb (2.6ms) 31 | Rendered layouts/_about.html.erb (0.3ms) 32 | Rendered layouts/_google_analytics.html.erb (0.4ms) 33 | Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms) 34 | ``` 35 | 36 | you get a single line with all the important information, like this: 37 | 38 | ``` 39 | method=GET path=/ format=json controller=HomeController action=index status=200 duration=79.0 view=78.8 db=0.0 40 | ``` 41 | 42 | The second line is easy to grasp with a single glance and still includes all the 43 | relevant information as simple key-value pairs. The syntax is heavily inspired 44 | by the log output of the Heroku router. It doesn't include any timestamp by 45 | default, instead it assumes you use a proper log formatter instead. 46 | 47 | ## Supported Ruby and Rails Releases ## 48 | 49 | Lograge is actively tested against current and officially supported Ruby and 50 | Rails releases. That said, Lograge _should_ work with older releases. 51 | 52 | - [Rails](https://endoflife.date/rails): Edge, 7.1, 7.0, 6.1, 6.0, 5.2 53 | - Rubies: 54 | - [MRI](https://endoflife.date/ruby): HEAD, 3.3, 3.2 3.1, 3.0, 2.7, 2.6 55 | - JRuby: HEAD, 9.2, 9.1 56 | - TruffleRuby: HEAD, 21.3 57 | 58 | ## Installation ## 59 | 60 | In your Gemfile 61 | 62 | ```ruby 63 | gem "lograge" 64 | ``` 65 | 66 | Enable it in an initializer or the relevant environment config: 67 | 68 | ```ruby 69 | # config/initializers/lograge.rb 70 | # OR 71 | # config/environments/production.rb 72 | Rails.application.configure do 73 | config.lograge.enabled = true 74 | end 75 | ``` 76 | 77 | If you're using Rails 5's API-only mode and inherit from 78 | `ActionController::API`, you must define it as the controller base class which 79 | lograge will patch: 80 | 81 | ```ruby 82 | # config/initializers/lograge.rb 83 | Rails.application.configure do 84 | config.lograge.base_controller_class = 'ActionController::API' 85 | end 86 | ``` 87 | 88 | If you use multiple base controller classes in your application, specify an array: 89 | 90 | ```ruby 91 | # config/initializers/lograge.rb 92 | Rails.application.configure do 93 | config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base'] 94 | end 95 | ``` 96 | 97 | You can also add a hook for own custom data 98 | 99 | ```ruby 100 | # config/environments/staging.rb 101 | Rails.application.configure do 102 | config.lograge.enabled = true 103 | 104 | # custom_options can be a lambda or hash 105 | # if it's a lambda then it must return a hash 106 | config.lograge.custom_options = lambda do |event| 107 | # capture some specific timing values you are interested in 108 | {:name => "value", :timing => some_float.round(2), :host => event.payload[:host]} 109 | end 110 | end 111 | ``` 112 | 113 | Or you can add a timestamp: 114 | 115 | ```ruby 116 | Rails.application.configure do 117 | config.lograge.enabled = true 118 | 119 | # add time to lograge 120 | config.lograge.custom_options = lambda do |event| 121 | { time: Time.now } 122 | end 123 | end 124 | ``` 125 | 126 | You can also keep the original (and verbose) Rails logger by following this configuration: 127 | 128 | ```ruby 129 | Rails.application.configure do 130 | config.lograge.keep_original_rails_log = true 131 | 132 | config.lograge.logger = ActiveSupport::Logger.new "#{Rails.root}/log/lograge_#{Rails.env}.log" 133 | end 134 | ``` 135 | 136 | You can then add custom variables to the event to be used in `custom_options` (available via the `event.payload` hash, which has to be processed in `custom_options` method to be included in log output, see above): 137 | 138 | ```ruby 139 | # app/controllers/application_controller.rb 140 | class ApplicationController < ActionController::Base 141 | def append_info_to_payload(payload) 142 | super 143 | payload[:host] = request.host 144 | end 145 | end 146 | ``` 147 | 148 | Alternatively, you can add a hook for accessing controller methods directly (e.g. `request` and `current_user`). 149 | This hash is merged into the log data automatically. 150 | 151 | ```ruby 152 | Rails.application.configure do 153 | config.lograge.enabled = true 154 | 155 | config.lograge.custom_payload do |controller| 156 | { 157 | host: controller.request.host, 158 | user_id: controller.current_user.try(:id) 159 | } 160 | end 161 | end 162 | ``` 163 | 164 | To further clean up your logging, you can also tell Lograge to skip log messages 165 | meeting given criteria. You can skip log messages generated from certain controller 166 | actions, or you can write a custom handler to skip messages based on data in the log event: 167 | 168 | ```ruby 169 | # config/environments/production.rb 170 | Rails.application.configure do 171 | config.lograge.enabled = true 172 | 173 | config.lograge.ignore_actions = ['HomeController#index', 'AController#an_action'] 174 | config.lograge.ignore_custom = lambda do |event| 175 | # return true here if you want to ignore based on the event 176 | end 177 | end 178 | ``` 179 | 180 | Lograge supports multiple output formats. The most common is the default 181 | lograge key-value format described above. Alternatively, you can also generate 182 | JSON logs in the json_event format used by [Logstash](http://logstash.net/). 183 | 184 | ```ruby 185 | # config/environments/production.rb 186 | Rails.application.configure do 187 | config.lograge.formatter = Lograge::Formatters::Logstash.new 188 | end 189 | ``` 190 | 191 | *Note:* When using the logstash output, you need to add the additional gem 192 | `logstash-event`. You can simply add it to your Gemfile like this 193 | 194 | ```ruby 195 | gem "logstash-event" 196 | ``` 197 | 198 | Done. 199 | 200 | The available formatters are: 201 | 202 | ```ruby 203 | Lograge::Formatters::Lines.new 204 | Lograge::Formatters::Cee.new 205 | Lograge::Formatters::Graylog2.new 206 | Lograge::Formatters::KeyValue.new # default lograge format 207 | Lograge::Formatters::KeyValueDeep.new 208 | Lograge::Formatters::Json.new 209 | Lograge::Formatters::Logstash.new 210 | Lograge::Formatters::LTSV.new 211 | Lograge::Formatters::Raw.new # Returns a ruby hash object 212 | ``` 213 | 214 | In addition to the formatters, you can manipulate the data yourself by passing 215 | an object which responds to #call: 216 | 217 | ```ruby 218 | # config/environments/production.rb 219 | Rails.application.configure do 220 | config.lograge.formatter = ->(data) { "Called #{data[:controller]}" } # data is a ruby hash 221 | end 222 | ``` 223 | 224 | ## Internals ## 225 | 226 | Thanks to the notification system that was introduced in Rails 3, replacing the 227 | logging is easy. Lograge unhooks all subscriptions from 228 | `ActionController::LogSubscriber` and `ActionView::LogSubscriber`, and hooks in 229 | its own log subscription, but only listening for two events: `process_action` 230 | and `redirect_to` (in case of standard controller logs). 231 | It makes sure that only subscriptions from those two classes 232 | are removed. If you happened to hook in your own, they'll be safe. 233 | 234 | Unfortunately, when a redirect is triggered by your application's code, 235 | ActionController fires two events. One for the redirect itself, and another one 236 | when the request is finished. Unfortunately, the final event doesn't include the 237 | redirect, so Lograge stores the redirect URL as a thread-local attribute and 238 | refers to it in `process_action`. 239 | 240 | The event itself contains most of the relevant information to build up the log 241 | line, including view processing and database access times. 242 | 243 | While the LogSubscribers encapsulate most logging pretty nicely, there are still 244 | two lines that show up no matter what. The first line that's output for every 245 | Rails request, you know, this one: 246 | 247 | ``` 248 | Started GET "/" for 127.0.0.1 at 2012-03-12 17:10:10 +0100 249 | ``` 250 | 251 | And the verbose output coming from rack-cache: 252 | 253 | ``` 254 | cache: [GET /] miss 255 | ``` 256 | 257 | Both are independent of the LogSubscribers, and both need to be shut up using 258 | different means. 259 | 260 | For the first one, the starting line of every Rails request log, Lograge 261 | replaces code in `Rails::Rack::Logger` to remove that particular log line. It's 262 | not great, but it's just another unnecessary output and would still clutter the 263 | log files. Maybe a future version of Rails will make this log line an event as 264 | well. 265 | 266 | To remove rack-cache's output (which is only enabled if caching in Rails is 267 | enabled), Lograge disables verbosity for rack-cache, which is unfortunately 268 | enabled by default. 269 | 270 | There, a single line per request. Beautiful. 271 | 272 | ## Action Cable ## 273 | 274 | Starting with version 0.11.0, Lograge introduced support for Action Cable logs. 275 | This proved to be a particular challenge since the framework code is littered 276 | with multiple (and seemingly random) logger calls in a number of internal classes. 277 | In order to deal with it, the default Action Cable logger was silenced. 278 | As a consequence, calling logger e.g. in user-defined `Connection` or `Channel` 279 | classes has no effect - `Rails.logger` (or any other logger instance) 280 | has to be used instead. 281 | 282 | Additionally, while standard controller logs rely on `process_action` and `redirect_to` 283 | instrumentations only, Action Cable messages are generated from multiple events: 284 | `perform_action`, `subscribe`, `unsubscribe`, `connect`, and `disconnect`. 285 | `perform_action` is the only one included in the actual Action Cable code and 286 | others have been added by monkey patching [`ActionCable::Channel::Base`](https://github.com/roidrage/lograge/blob/master/lib/lograge/rails_ext/action_cable/channel/base.rb) and 287 | [`ActionCable::Connection::Base`](https://github.com/roidrage/lograge/blob/master/lib/lograge/rails_ext/action_cable/connection/base.rb) classes. 288 | 289 | ## What it doesn't do ## 290 | 291 | Lograge is opinionated, very opinionated. If the stuff below doesn't suit your 292 | needs, it may not be for you. 293 | 294 | Lograge removes ActionView logging, which also includes rendering times for 295 | partials. If you're into those, Lograge is probably not for you. In my honest 296 | opinion, those rendering times don't belong in the log file, they should be 297 | collected in a system like New Relic, Librato Metrics or some other metrics 298 | service that allows graphing rendering percentiles. I assume this for everything 299 | that represents a moving target. That kind of data is better off being 300 | visualized in graphs than dumped (and ignored) in a log file. 301 | 302 | Lograge doesn't yet log the request parameters. This is something I'm actively 303 | contemplating, mainly because I want to find a good way to include them, a way 304 | that fits in with the general spirit of the log output generated by Lograge. 305 | If you decide to include them be sure that sensitive data like passwords 306 | and credit cards are not stored via [filtered_parameters](https://api.rubyonrails.org/classes/ActionDispatch/Http/FilterParameters.html) 307 | or another means. The payload does already contain the params hash, so you can easily add 308 | it in manually using `custom_options`: 309 | 310 | ```ruby 311 | # production.rb 312 | YourApp::Application.configure do 313 | config.lograge.enabled = true 314 | config.lograge.custom_options = lambda do |event| 315 | exceptions = %w(controller action format id) 316 | { 317 | params: event.payload[:params].except(*exceptions) 318 | } 319 | end 320 | end 321 | ``` 322 | 323 | ## FAQ ## 324 | 325 | ### Logging errors / exceptions ### 326 | 327 | Our first recommendation is that you use exception tracking services built for 328 | purpose ;) 329 | 330 | If you absolutely *must* log exceptions in the single-line format, you can 331 | do something similar to this example: 332 | 333 | ```ruby 334 | # config/environments/production.rb 335 | 336 | YourApp::Application.configure do 337 | config.lograge.enabled = true 338 | config.lograge.custom_options = lambda do |event| 339 | { 340 | exception: event.payload[:exception], # ["ExceptionClass", "the message"] 341 | exception_object: event.payload[:exception_object] # the exception instance 342 | } 343 | end 344 | end 345 | ``` 346 | 347 | The `:exception` is just the basic class and message whereas the 348 | `:exception_object` is the actual exception instance. You can use both / 349 | either. Be mindful when including this, you will probably want to cherry-pick 350 | particular attributes and almost definitely want to `join` the `backtrace` into 351 | something without newline characters. 352 | 353 | ### Handle ActionController::RoutingError ### 354 | 355 | Add a ` get '*unmatched_route', to: 'application#route_not_found'` rule to the end of your `routes.rb` 356 | Then add a new controller action in your `application_controller.rb`. 357 | 358 | ```ruby 359 | def route_not_found 360 | render 'error_pages/404', status: :not_found 361 | end 362 | ``` 363 | 364 | [#146](https://github.com/roidrage/lograge/issues/146) 365 | 366 | ## Alternative & Related Projects 367 | 368 | * [`rails_semantic_logger` is a similar project with different functionality](https://logger.rocketjob.io/rails). 369 | * [`simple_structured_logger`](https://github.com/iloveitaly/simple_structured_logger) adds structured logging to the rest of your application 370 | 371 | ## Contributing ## 372 | 373 | See the CONTRIBUTING.md file for further information. 374 | 375 | ## License ## 376 | 377 | MIT. Code extracted from [Travis CI](http://travis-ci.org). 378 | 379 | (c) Mathias Meyer 380 | 381 | See `LICENSE.txt` for details. 382 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new 7 | 8 | require 'rubocop/rake_task' 9 | RuboCop::RakeTask.new 10 | 11 | desc 'Run specs, rubocop and reek' 12 | task ci: %w[spec rubocop] 13 | 14 | task default: :ci 15 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', '~> 5.2.0' 10 | gem 'activerecord', '~> 5.2.0' 11 | 12 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 13 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 14 | # we test against the correct code. 15 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 16 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 17 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 18 | gem 'lines' 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', '~> 6.0.0' 10 | gem 'activerecord', '~> 6.0.0' 11 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 12 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 13 | # we test against the correct code. 14 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 15 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 16 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 17 | gem 'lines' 18 | end 19 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', '~> 6.1.0' 10 | gem 'activerecord', '~> 6.1.0' 11 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 12 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 13 | # we test against the correct code. 14 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 15 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 16 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 17 | gem 'lines' 18 | gem 'thread_safe' 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', '~> 7.0.0' 10 | gem 'activerecord', '~> 7.0.0' 11 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 12 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 13 | # we test against the correct code. 14 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 15 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 16 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 17 | gem 'lines' 18 | gem 'thread_safe' 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', '~> 7.1.0' 10 | gem 'activerecord', '~> 7.1.0' 11 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 12 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 13 | # we test against the correct code. 14 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 15 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 16 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 17 | gem 'lines' 18 | gem 'thread_safe' 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/rails_edge.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in lograge.gemspec 6 | gemspec path: '..' 7 | 8 | group :test do 9 | gem 'actionpack', github: 'rails/rails' 10 | gem 'activerecord', github: 'rails/rails' 11 | # logstash does not release any gems on rubygems, but they have two gemspecs within their repo. 12 | # Using the tag is an attempt of having a stable version to test against where we can ensure that 13 | # we test against the correct code. 14 | gem 'logstash-event', git: 'https://github.com/elastic/logstash', tag: 'v1.5.4' 15 | # logstash 1.5.4 is only supported with jrjackson up to 0.2.9 16 | gem 'jrjackson', '~> 0.2.9', platforms: :jruby 17 | gem 'lines' 18 | gem 'thread_safe' 19 | end 20 | -------------------------------------------------------------------------------- /lib/lograge.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lograge/version' 4 | require 'lograge/formatters/helpers/method_and_path' 5 | require 'lograge/formatters/cee' 6 | require 'lograge/formatters/json' 7 | require 'lograge/formatters/graylog2' 8 | require 'lograge/formatters/key_value' 9 | require 'lograge/formatters/key_value_deep' 10 | require 'lograge/formatters/l2met' 11 | require 'lograge/formatters/lines' 12 | require 'lograge/formatters/logstash' 13 | require 'lograge/formatters/ltsv' 14 | require 'lograge/formatters/raw' 15 | require 'lograge/log_subscribers/base' 16 | require 'lograge/log_subscribers/action_cable' 17 | require 'lograge/log_subscribers/action_controller' 18 | require 'lograge/silent_logger' 19 | require 'lograge/ordered_options' 20 | require 'active_support' 21 | require 'active_support/core_ext/module/attribute_accessors' 22 | require 'active_support/core_ext/string/inflections' 23 | 24 | # rubocop:disable Metrics/ModuleLength 25 | module Lograge 26 | module_function 27 | 28 | mattr_accessor :logger, :application, :ignore_tests 29 | 30 | # Custom options that will be appended to log line 31 | # 32 | # Currently supported formats are: 33 | # - Hash 34 | # - Any object that responds to call and returns a hash 35 | # 36 | mattr_writer :custom_options 37 | self.custom_options = nil 38 | 39 | def custom_options(event) 40 | if @@custom_options.respond_to?(:call) 41 | @@custom_options.call(event) 42 | else 43 | @@custom_options 44 | end 45 | end 46 | 47 | # Before format allows you to change the structure of the output. 48 | # You've to pass in something callable 49 | # 50 | mattr_writer :before_format 51 | self.before_format = nil 52 | 53 | def before_format(data, payload) 54 | result = nil 55 | result = @@before_format.call(data, payload) if @@before_format 56 | result || data 57 | end 58 | 59 | # Set conditions for events that should be ignored 60 | # 61 | # Currently supported formats are: 62 | # - A single string representing a controller action, e.g. 'UsersController#sign_in' 63 | # - An array of strings representing controller actions 64 | # - An object that responds to call with an event argument and returns 65 | # true iff the event should be ignored. 66 | # 67 | # The action ignores are given to 'ignore_actions'. The callable ignores 68 | # are given to 'ignore'. Both methods can be called multiple times, which 69 | # just adds more ignore conditions to a list that is checked before logging. 70 | 71 | def ignore_actions(actions) 72 | ignore(lambda do |event| 73 | params = event.payload 74 | Array(actions).include?("#{controller_field(params)}##{params[:action]}") 75 | end) 76 | end 77 | 78 | def controller_field(params) 79 | params[:controller] || params[:channel_class] || params[:connection_class] 80 | end 81 | 82 | def ignore_tests 83 | @ignore_tests ||= [] 84 | end 85 | 86 | def ignore(test) 87 | ignore_tests.push(test) if test 88 | end 89 | 90 | def ignore_nothing 91 | @ignore_tests = [] 92 | end 93 | 94 | def ignore?(event) 95 | ignore_tests.any? { |ignore_test| ignore_test.call(event) } 96 | end 97 | 98 | # Loglines are emitted with this log level 99 | mattr_accessor :log_level 100 | self.log_level = :info 101 | 102 | # The emitted log format 103 | # 104 | # Currently supported formats are> 105 | # - :lograge - The custom tense lograge format 106 | # - :logstash - JSON formatted as a Logstash Event. 107 | mattr_accessor :formatter 108 | 109 | def remove_existing_log_subscriptions 110 | ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber| 111 | case subscriber 112 | when ActionView::LogSubscriber 113 | unsubscribe(:action_view, subscriber) 114 | when ActionController::LogSubscriber 115 | unsubscribe(:action_controller, subscriber) 116 | end 117 | end 118 | end 119 | 120 | def unsubscribe(component, subscriber) 121 | events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' } 122 | events.each do |event| 123 | Lograge.notification_listeners_for("#{event}.#{component}").each do |listener| 124 | ActiveSupport::Notifications.unsubscribe listener if listener.instance_variable_get('@delegate') == subscriber 125 | end 126 | end 127 | end 128 | 129 | def setup(app) 130 | self.application = app 131 | disable_rack_cache_verbose_output 132 | keep_original_rails_log 133 | 134 | attach_to_action_controller 135 | attach_to_action_cable if defined?(ActionCable) 136 | 137 | set_lograge_log_options 138 | setup_custom_payload 139 | support_deprecated_config # TODO: Remove with version 1.0 140 | set_formatter 141 | set_ignores 142 | end 143 | 144 | def set_ignores 145 | Lograge.ignore_actions(lograge_config.ignore_actions) 146 | Lograge.ignore(lograge_config.ignore_custom) 147 | end 148 | 149 | def set_formatter 150 | Lograge.formatter = lograge_config.formatter || Lograge::Formatters::KeyValue.new 151 | end 152 | 153 | def attach_to_action_controller 154 | Lograge::LogSubscribers::ActionController.attach_to :action_controller 155 | end 156 | 157 | def attach_to_action_cable 158 | require 'lograge/rails_ext/action_cable/channel/base' 159 | require 'lograge/rails_ext/action_cable/connection/base' 160 | 161 | Lograge::LogSubscribers::ActionCable.attach_to :action_cable 162 | end 163 | 164 | def setup_custom_payload 165 | return unless lograge_config.custom_payload_method.respond_to?(:call) 166 | 167 | base_classes = Array(lograge_config.base_controller_class) 168 | base_classes.map! { |klass| klass.try(:constantize) } 169 | base_classes << ActionController::Base if base_classes.empty? 170 | 171 | base_classes.each do |base_class| 172 | extend_base_class(base_class) 173 | end 174 | end 175 | 176 | def extend_base_class(klass) 177 | append_payload_method = klass.instance_method(:append_info_to_payload) 178 | custom_payload_method = lograge_config.custom_payload_method 179 | 180 | klass.send(:define_method, :append_info_to_payload) do |payload| 181 | append_payload_method.bind(self).call(payload) 182 | payload[:custom_payload] = custom_payload_method.call(self) 183 | end 184 | end 185 | 186 | def set_lograge_log_options 187 | Lograge.logger = lograge_config.logger 188 | Lograge.custom_options = lograge_config.custom_options 189 | Lograge.before_format = lograge_config.before_format 190 | Lograge.log_level = lograge_config.log_level || :info 191 | end 192 | 193 | def disable_rack_cache_verbose_output 194 | application.config.action_dispatch.rack_cache[:verbose] = false if rack_cache_hashlike?(application) 195 | end 196 | 197 | def keep_original_rails_log 198 | return if lograge_config.keep_original_rails_log 199 | 200 | require 'lograge/rails_ext/rack/logger' 201 | 202 | require 'lograge/rails_ext/action_cable/server/base' if defined?(ActionCable) 203 | 204 | Lograge.remove_existing_log_subscriptions 205 | end 206 | 207 | def rack_cache_hashlike?(app) 208 | app.config.action_dispatch.rack_cache&.respond_to?(:[]=) 209 | end 210 | private_class_method :rack_cache_hashlike? 211 | 212 | # TODO: Remove with version 1.0 213 | 214 | def support_deprecated_config 215 | return unless lograge_config.log_format 216 | 217 | legacy_log_format = lograge_config.log_format 218 | warning = 'config.lograge.log_format is deprecated. Use config.lograge.formatter instead.' 219 | deprecator.warn(warning, caller) 220 | legacy_log_format = :key_value if legacy_log_format == :lograge 221 | lograge_config.formatter = "Lograge::Formatters::#{legacy_log_format.to_s.classify}".constantize.new 222 | end 223 | 224 | def lograge_config 225 | application.config.lograge 226 | end 227 | 228 | def deprecator 229 | @deprecator ||= ActiveSupport::Deprecation.new('1.0', 'Lograge') 230 | end 231 | 232 | if ::ActiveSupport::VERSION::MAJOR >= 8 || 233 | (::ActiveSupport::VERSION::MAJOR >= 7 && ::ActiveSupport::VERSION::MINOR >= 1) 234 | def notification_listeners_for(name) 235 | ActiveSupport::Notifications.notifier.all_listeners_for(name) 236 | end 237 | else 238 | def notification_listeners_for(name) 239 | ActiveSupport::Notifications.notifier.listeners_for(name) 240 | end 241 | end 242 | end 243 | # rubocop:enable Metrics/ModuleLength 244 | 245 | require 'lograge/railtie' if defined?(Rails) 246 | -------------------------------------------------------------------------------- /lib/lograge/formatters/cee.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class Cee 6 | def call(data) 7 | "@cee: #{JSON.dump(data)}" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/lograge/formatters/graylog2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class Graylog2 6 | include Lograge::Formatters::Helpers::MethodAndPath 7 | 8 | def call(data) 9 | # Add underscore to every key to follow GELF additional field syntax. 10 | data.transform_keys { |k| underscore_prefix(k) }.merge( 11 | short_message: short_message(data) 12 | ) 13 | end 14 | 15 | def underscore_prefix(key) 16 | :"_#{key}" 17 | end 18 | 19 | def short_message(data) 20 | "[#{data[:status]}]#{method_and_path_string(data)}(#{data[:controller]}##{data[:action]})" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lograge/formatters/helpers/method_and_path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | module Helpers 6 | module MethodAndPath 7 | def method_and_path_string(data) 8 | method_and_path = [data[:method], data[:path]].compact 9 | method_and_path.any?(&:present?) ? " #{method_and_path.join(' ')} " : ' ' 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/lograge/formatters/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | module Lograge 5 | module Formatters 6 | class Json 7 | def call(data) 8 | ::JSON.dump(data) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lograge/formatters/key_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class KeyValue 6 | def call(data) 7 | fields_to_display(data) 8 | .map { |key| format(key, data[key]) } 9 | .join(' ') 10 | end 11 | 12 | protected 13 | 14 | def fields_to_display(data) 15 | data.keys 16 | end 17 | 18 | def format(key, value) 19 | "#{key}=#{parse_value(key, value)}" 20 | end 21 | 22 | def parse_value(key, value) 23 | # Exactly preserve the previous output 24 | # Parsing this can be ambiguous if the error messages contains 25 | # a single quote 26 | return "'#{value}'" if key == :error 27 | return Kernel.format('%.2f', value) if value.is_a? Float 28 | 29 | value 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/lograge/formatters/key_value_deep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class KeyValueDeep < KeyValue 6 | def call(data) 7 | super(flatten_keys(data)) 8 | end 9 | 10 | protected 11 | 12 | def flatten_keys(data, prefix = '') 13 | return flatten_object(data, prefix) if [Hash, Array].include? data.class 14 | 15 | data 16 | end 17 | 18 | def flatten_object(data, prefix) 19 | result = {} 20 | loop_on_object(data) do |key, value| 21 | key = "#{prefix}_#{key}" unless prefix.empty? 22 | if [Hash, Array].include? value.class 23 | result.merge!(flatten_keys(value, key)) 24 | else 25 | result[key] = value 26 | end 27 | end 28 | result 29 | end 30 | 31 | def loop_on_object(data, &block) 32 | if data.instance_of? Array 33 | data.each_with_index do |value, index| 34 | yield index, value 35 | end 36 | return 37 | end 38 | data.each(&block) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/lograge/formatters/l2met.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lograge/formatters/key_value' 4 | 5 | module Lograge 6 | module Formatters 7 | class L2met < KeyValue 8 | L2MET_FIELDS = %i[ 9 | method 10 | path 11 | format 12 | source 13 | status 14 | error 15 | duration 16 | view 17 | db 18 | location 19 | ].freeze 20 | 21 | UNWANTED_FIELDS = %i[ 22 | controller 23 | action 24 | ].freeze 25 | 26 | def call(data) 27 | super(modify_payload(data)) 28 | end 29 | 30 | protected 31 | 32 | def fields_to_display(data) 33 | L2MET_FIELDS + additional_fields(data) 34 | end 35 | 36 | def additional_fields(data) 37 | (data.keys - L2MET_FIELDS) - UNWANTED_FIELDS 38 | end 39 | 40 | def format(key, value) 41 | key = "measure#page.#{key}" if value.is_a?(Float) 42 | 43 | super 44 | end 45 | 46 | def modify_payload(data) 47 | data[:source] = source_field(data) if data[:controller] && data[:action] 48 | 49 | data 50 | end 51 | 52 | def source_field(data) 53 | "#{data[:controller].to_s.tr('/', '-')}:#{data[:action]}" 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/lograge/formatters/lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class Lines 6 | def call(data) 7 | load_dependencies 8 | 9 | ::Lines.dump(data) 10 | end 11 | 12 | def load_dependencies 13 | require 'lines' 14 | rescue LoadError 15 | puts 'You need to install the lines gem to use this output.' 16 | raise 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lograge/formatters/logstash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class Logstash 6 | include Lograge::Formatters::Helpers::MethodAndPath 7 | 8 | def call(data) 9 | load_dependencies 10 | event = LogStash::Event.new(data) 11 | 12 | event['message'] = "[#{data[:status]}]#{method_and_path_string(data)}(#{data[:controller]}##{data[:action]})" 13 | event.to_json 14 | end 15 | 16 | def load_dependencies 17 | require 'logstash-event' 18 | rescue LoadError 19 | puts 'You need to install the logstash-event gem to use the logstash output.' 20 | raise 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lograge/formatters/ltsv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class LTSV 6 | def call(data) 7 | fields = fields_to_display(data) 8 | 9 | event = fields.map { |key| format(key, data[key]) } 10 | event.join("\t") 11 | end 12 | 13 | def fields_to_display(data) 14 | data.keys 15 | end 16 | 17 | def format(key, value) 18 | if key == :error 19 | # Exactly preserve the previous output 20 | # Parsing this can be ambiguous if the error messages contains 21 | # a single quote 22 | value = "'#{escape value}'" 23 | elsif value.is_a? Float 24 | value = Kernel.format('%.2f', value) 25 | end 26 | 27 | "#{key}:#{value}" 28 | end 29 | 30 | private 31 | 32 | def escape(string) 33 | value = string.is_a?(String) ? string.dup : string.to_s 34 | 35 | value.gsub!('\\', '\\\\') 36 | value.gsub!('\n', '\\n') 37 | value.gsub!('\r', '\\r') 38 | value.gsub!('\t', '\\t') 39 | 40 | value 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/lograge/formatters/raw.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module Formatters 5 | class Raw 6 | def call(data) 7 | data 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/lograge/log_subscribers/action_cable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module LogSubscribers 5 | class ActionCable < Base 6 | %i[perform_action subscribe unsubscribe connect disconnect].each do |method_name| 7 | define_method(method_name) do |event| 8 | process_main_event(event) 9 | end 10 | end 11 | 12 | private 13 | 14 | def initial_data(payload) 15 | { 16 | method: nil, 17 | path: nil, 18 | format: nil, 19 | params: payload[:data], 20 | controller: payload[:channel_class] || payload[:connection_class], 21 | action: payload[:action] 22 | } 23 | end 24 | 25 | def default_status 26 | 200 27 | end 28 | 29 | def extract_runtimes(event, _payload) 30 | { duration: event.duration.to_f.round(2) } 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/lograge/log_subscribers/action_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module LogSubscribers 5 | class ActionController < Base 6 | def process_action(event) 7 | process_main_event(event) 8 | end 9 | 10 | def redirect_to(event) 11 | RequestStore.store[:lograge_location] = event.payload[:location] 12 | end 13 | 14 | def unpermitted_parameters(event) 15 | RequestStore.store[:lograge_unpermitted_params] ||= [] 16 | RequestStore.store[:lograge_unpermitted_params].concat(event.payload[:keys]) 17 | end 18 | 19 | private 20 | 21 | def initial_data(payload) 22 | { 23 | method: payload[:method], 24 | path: extract_path(payload), 25 | format: extract_format(payload), 26 | controller: payload[:controller], 27 | action: payload[:action] 28 | } 29 | end 30 | 31 | def extract_path(payload) 32 | path = payload[:path] 33 | strip_query_string(path) 34 | end 35 | 36 | def strip_query_string(path) 37 | index = path.index('?') 38 | index ? path[0, index] : path 39 | end 40 | 41 | if ::ActionPack::VERSION::MAJOR == 3 && ::ActionPack::VERSION::MINOR.zero? 42 | def extract_format(payload) 43 | payload[:formats].first 44 | end 45 | else 46 | def extract_format(payload) 47 | payload[:format] 48 | end 49 | end 50 | 51 | def extract_runtimes(event, payload) 52 | data = { duration: event.duration.to_f.round(2) } 53 | data[:view] = payload[:view_runtime].to_f.round(2) if payload.key?(:view_runtime) 54 | data[:db] = payload[:db_runtime].to_f.round(2) if payload.key?(:db_runtime) 55 | data 56 | end 57 | 58 | def extract_location 59 | location = RequestStore.store[:lograge_location] 60 | return {} unless location 61 | 62 | RequestStore.store[:lograge_location] = nil 63 | { location: strip_query_string(location) } 64 | end 65 | 66 | def extract_unpermitted_params 67 | unpermitted_params = RequestStore.store[:lograge_unpermitted_params] 68 | return {} unless unpermitted_params 69 | 70 | RequestStore.store[:lograge_unpermitted_params] = nil 71 | { unpermitted_params: unpermitted_params } 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/lograge/log_subscribers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'action_pack' 5 | require 'active_support' 6 | require 'active_support/core_ext/class/attribute' 7 | require 'active_support/log_subscriber' 8 | require 'request_store' 9 | 10 | module Lograge 11 | module LogSubscribers 12 | class Base < ActiveSupport::LogSubscriber 13 | def logger 14 | Lograge.logger.presence || super 15 | end 16 | 17 | private 18 | 19 | def process_main_event(event) 20 | return if Lograge.ignore?(event) 21 | 22 | payload = event.payload 23 | data = extract_request(event, payload) 24 | data = before_format(data, payload) 25 | formatted_message = Lograge.formatter.call(data) 26 | logger.send(Lograge.log_level, formatted_message) 27 | end 28 | 29 | def extract_request(event, payload) 30 | data = initial_data(payload) 31 | data.merge!(extract_status(payload)) 32 | data.merge!(extract_allocations(event)) 33 | data.merge!(extract_runtimes(event, payload)) 34 | data.merge!(extract_location) 35 | data.merge!(extract_unpermitted_params) 36 | data.merge!(custom_options(event)) 37 | end 38 | 39 | %i[initial_data extract_status extract_runtimes 40 | extract_location extract_unpermitted_params].each do |method_name| 41 | define_method(method_name) { |*_arg| {} } 42 | end 43 | 44 | def extract_status(payload) 45 | if (status = payload[:status]) 46 | { status: status.to_i } 47 | elsif (error = payload[:exception]) 48 | exception, message = error 49 | { status: get_error_status_code(exception), error: "#{exception}: #{message}" } 50 | else 51 | { status: default_status } 52 | end 53 | end 54 | 55 | def default_status 56 | 0 57 | end 58 | 59 | def get_error_status_code(exception_class_name) 60 | ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) 61 | end 62 | 63 | def extract_allocations(event) 64 | if (allocations = event.respond_to?(:allocations) && event.allocations) 65 | { allocations: allocations } 66 | else 67 | {} 68 | end 69 | end 70 | 71 | def custom_options(event) 72 | options = Lograge.custom_options(event) || {} 73 | options.merge event.payload[:custom_payload] || {} 74 | end 75 | 76 | def before_format(data, payload) 77 | Lograge.before_format(data, payload) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/lograge/ordered_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support' 4 | require 'active_support/ordered_options' 5 | 6 | module Lograge 7 | class OrderedOptions < ActiveSupport::OrderedOptions 8 | def custom_payload(&block) 9 | self.custom_payload_method = block 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lograge/rails_ext/action_cable/channel/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module ActionCable 5 | module ChannelInstrumentation 6 | def subscribe_to_channel 7 | ActiveSupport::Notifications.instrument('subscribe.action_cable', notification_payload('subscribe')) { super } 8 | end 9 | 10 | def unsubscribe_from_channel 11 | ActiveSupport::Notifications.instrument('unsubscribe.action_cable', notification_payload('unsubscribe')) do 12 | super 13 | end 14 | end 15 | 16 | private 17 | 18 | def notification_payload(method_name) 19 | { channel_class: self.class.name, action: method_name } 20 | end 21 | end 22 | end 23 | end 24 | 25 | ActionCable::Channel::Base.prepend(Lograge::ActionCable::ChannelInstrumentation) 26 | -------------------------------------------------------------------------------- /lib/lograge/rails_ext/action_cable/connection/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | module ActionCable 5 | module ConnectionInstrumentation 6 | def handle_open 7 | ActiveSupport::Notifications.instrument('connect.action_cable', notification_payload('connect')) { super } 8 | end 9 | 10 | def handle_close 11 | ActiveSupport::Notifications.instrument('disconnect.action_cable', notification_payload('disconnect')) { super } 12 | end 13 | 14 | def notification_payload(method_name) 15 | { connection_class: self.class.name, action: method_name, data: request.params } 16 | end 17 | end 18 | end 19 | end 20 | 21 | ActionCable::Connection::Base.prepend(Lograge::ActionCable::ConnectionInstrumentation) 22 | -------------------------------------------------------------------------------- /lib/lograge/rails_ext/action_cable/server/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActionCable 4 | module Server 5 | class Base 6 | mattr_accessor :logger 7 | self.logger = Lograge::SilentLogger.new(config.logger) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lograge/rails_ext/rack/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support' 4 | require 'active_support/concern' 5 | require 'rails/rack/logger' 6 | 7 | module Rails 8 | module Rack 9 | # Overwrites defaults of Rails::Rack::Logger that cause 10 | # unnecessary logging. 11 | # This effectively removes the log lines from the log 12 | # that say: 13 | # Started GET / for 192.168.2.1... 14 | class Logger 15 | # Overwrites Rails code that logs new requests 16 | def call_app(*args) 17 | env = args.last 18 | status, headers, body = @app.call(env) 19 | # needs to have same return type as the Rails builtins being overridden, see https://github.com/roidrage/lograge/pull/333 20 | # https://github.com/rails/rails/blob/be9d34b9bcb448b265114ebc28bef1a5b5e4c272/railties/lib/rails/rack/logger.rb#L37 21 | [status, headers, ::Rack::BodyProxy.new(body) {}] # rubocop:disable Lint/EmptyBlock 22 | ensure 23 | ActiveSupport::LogSubscriber.flush_all! 24 | end 25 | 26 | # Overwrites Rails 3.0/3.1 code that logs new requests 27 | def before_dispatch(_env); end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/lograge/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/railtie' 4 | require 'action_view/log_subscriber' 5 | require 'action_controller/log_subscriber' 6 | 7 | module Lograge 8 | class Railtie < Rails::Railtie 9 | config.lograge = Lograge::OrderedOptions.new 10 | config.lograge.enabled = false 11 | 12 | initializer :deprecator do |app| 13 | app.deprecators[:lograge] = Lograge.deprecator if app.respond_to?(:deprecators) 14 | end 15 | 16 | config.after_initialize do |app| 17 | Lograge.setup(app) if app.config.lograge.enabled 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lograge/silent_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'delegate' 4 | 5 | module Lograge 6 | class SilentLogger < SimpleDelegator 7 | %i[debug info warn error fatal unknown].each do |method_name| 8 | # rubocop:disable Lint/EmptyBlock 9 | define_method(method_name) { |*_args| } 10 | # rubocop:enable Lint/EmptyBlock 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lograge/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lograge 4 | VERSION = '0.14.0' 5 | end 6 | -------------------------------------------------------------------------------- /lograge.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './lib/lograge/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'lograge' 7 | s.version = Lograge::VERSION 8 | s.authors = ['Mathias Meyer', 'Ben Lovell', 'Michael Bianco'] 9 | s.email = ['meyer@paperplanes.de', 'benjamin.lovell@gmail.com', 'mike@mikebian.co'] 10 | s.homepage = 'https://github.com/roidrage/lograge' 11 | s.summary = "Tame Rails' multi-line logging into a single line per request" 12 | s.description = "Tame Rails' multi-line logging into a single line per request" 13 | s.license = 'MIT' 14 | 15 | s.metadata = { 16 | 'rubygems_mfa_required' => 'true', 17 | 'changelog_uri' => 'https://github.com/roidrage/lograge/blob/master/CHANGELOG.md' 18 | } 19 | 20 | # NOTE(ivy): Ruby version 2.5 is the oldest syntax supported by Rubocop. 21 | s.required_ruby_version = '>= 2.5' 22 | 23 | s.files = `git ls-files lib LICENSE.txt`.split("\n") 24 | 25 | s.add_development_dependency 'base64' 26 | s.add_development_dependency 'mutex_m' 27 | s.add_development_dependency 'rspec', '~> 3.1' 28 | s.add_development_dependency 'rubocop', '~> 1.23' 29 | s.add_development_dependency 'simplecov', '~> 0.21' 30 | 31 | s.add_runtime_dependency 'actionpack', '>= 4' 32 | s.add_runtime_dependency 'activesupport', '>= 4' 33 | s.add_runtime_dependency 'railties', '>= 4' 34 | s.add_runtime_dependency 'request_store', '~> 1.0' 35 | end 36 | -------------------------------------------------------------------------------- /spec/formatters/cee_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Cee do 4 | it 'prepends the output with @cee' do 5 | expect(subject.call({})).to match(/^@cee/) 6 | end 7 | 8 | it 'serializes custom attributes' do 9 | expect(subject.call(custom: 'data')).to match('{"custom":"data"}') 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/formatters/graylog2_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Graylog2 do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index' 12 | } 13 | end 14 | 15 | subject { described_class.new.call(payload) } 16 | 17 | it "provides the ':_custom' attribute" do 18 | expect(subject[:_custom]).to eq('data') 19 | end 20 | 21 | it "provides the serialized ':short_message' attribute" do 22 | expect(subject[:short_message]).to eq('[200] GET / (welcome#index)') 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/formatters/helpers/method_and_path_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Helpers::MethodAndPath do 4 | describe '#method_and_path_string' do 5 | let(:instance) do 6 | Object.new.extend(described_class) 7 | end 8 | 9 | let(:method_and_path_string) { instance.method_and_path_string(data) } 10 | 11 | context "when both 'method' and 'path' fields are blank" do 12 | let(:data) { {} } 13 | 14 | it 'returns single space' do 15 | expect(method_and_path_string).to eq(' ') 16 | end 17 | end 18 | 19 | context "when 'method' field is present" do 20 | let(:data) { { method: 'GET' } } 21 | 22 | it "returns 'method' value surrounded with spaces" do 23 | expect(method_and_path_string).to eq(' GET ') 24 | end 25 | end 26 | 27 | context "when 'path' field is present" do 28 | let(:data) { { path: '/foo' } } 29 | 30 | it "returns 'path' value surrounded by spaces" do 31 | expect(method_and_path_string).to eq(' /foo ') 32 | end 33 | end 34 | 35 | context "when both 'method' and path' fields are present" do 36 | let(:data) { { method: 'index', path: '/foo' } } 37 | 38 | it 'returns string surrounded by spaces with both fields separated with a space ' do 39 | expect(method_and_path_string).to eq(' index /foo ') 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/formatters/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Json do 4 | let(:deserialized_output) { JSON.parse(subject.call(custom: 'data')) } 5 | 6 | it 'serializes custom attributes' do 7 | expect(deserialized_output).to eq('custom' => 'data') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/formatters/key_value_deep_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::KeyValueDeep do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index', 12 | params: { 13 | object: { 14 | key: 'value', 15 | key_array: [1, '2', 3.4] 16 | } 17 | } 18 | } 19 | end 20 | 21 | subject { described_class.new.call(payload) } 22 | 23 | it "includes the 'controller' key/value" do 24 | expect(subject).to include('controller=welcome') 25 | end 26 | 27 | it "includes the 'action' key/value" do 28 | expect(subject).to include('action=index') 29 | end 30 | 31 | it "includes the 'params_object_key' key/value" do 32 | expect(subject).to include('params_object_key=value') 33 | end 34 | 35 | it "includes the 'params_object_key_array_1' key/value" do 36 | expect(subject).to include('params_object_key_array_1=2') 37 | end 38 | 39 | it 'returns the correct serialization' do 40 | expect(subject).to eq("custom=data status=200 method=GET path=/ \ 41 | controller=welcome action=index params_object_key=value params_object_key_array_0=1 \ 42 | params_object_key_array_1=2 params_object_key_array_2=3.40") 43 | end 44 | 45 | it_behaves_like 'a key value formatter' 46 | end 47 | -------------------------------------------------------------------------------- /spec/formatters/key_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::KeyValue do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index' 12 | } 13 | end 14 | 15 | subject { described_class.new.call(payload) } 16 | 17 | it "includes the 'controller' key/value" do 18 | expect(subject).to include('controller=welcome') 19 | end 20 | 21 | it "includes the 'action' key/value" do 22 | expect(subject).to include('action=index') 23 | end 24 | 25 | it_behaves_like 'a key value formatter' 26 | end 27 | -------------------------------------------------------------------------------- /spec/formatters/l2met_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::L2met do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'admin/welcome', 11 | action: 'index', 12 | db: 20.00, 13 | view: 10.00, 14 | duration: 30.00, 15 | cache: 40.00 16 | } 17 | end 18 | 19 | it_behaves_like 'a key value formatter' 20 | 21 | subject { described_class.new.call(payload) } 22 | 23 | it "includes the 'source' key/value" do 24 | expect(subject).to include('source=admin-welcome:index') 25 | end 26 | 27 | it "does not include the 'controller' key/value" do 28 | expect(subject).not_to include('controller=admin/welcome') 29 | end 30 | 31 | it "does not include the 'action' key/value" do 32 | expect(subject).not_to include('action=index') 33 | end 34 | 35 | it "includes the 'page.duration'" do 36 | expect(subject).to include('measure#page.duration=30.00') 37 | end 38 | 39 | it "includes the 'page.view'" do 40 | expect(subject).to include('measure#page.view=10.00') 41 | end 42 | 43 | it "includes the 'page.db'" do 44 | expect(subject).to include('measure#page.db=20.00') 45 | end 46 | 47 | it "includes the 'page.cache'" do 48 | expect(subject).to include('measure#page.cache=40.00') 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/formatters/lines_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Lines do 4 | it 'can serialize custom data' do 5 | expect(subject.call(custom: 'data')).to eq('custom=data') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/formatters/logstash_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Logstash do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index' 12 | } 13 | end 14 | 15 | subject { described_class.new.call(payload) } 16 | 17 | it "includes the 'custom' key/value" do 18 | expect(subject).to match(/"custom":"data"/) 19 | end 20 | 21 | it "includes the 'status' key/value" do 22 | expect(subject).to match(/"status":200/) 23 | end 24 | 25 | it "includes the 'method' key/value" do 26 | expect(subject).to match(/"method":"GET"/) 27 | end 28 | 29 | it "includes the 'path' key/value" do 30 | expect(subject).to match(%r{"path":"/"}) 31 | end 32 | 33 | it "includes the 'controller' key/value" do 34 | expect(subject).to match(/"controller":"welcome"/) 35 | end 36 | 37 | it "includes the 'action' key/value" do 38 | expect(subject).to match(/"action":"index"/) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/formatters/ltsv_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::LTSV do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index', 12 | will_escaped: '\t' 13 | } 14 | end 15 | 16 | subject { described_class.new.call(payload) } 17 | 18 | it "includes the 'controller' key:value" do 19 | expect(subject).to include('controller:welcome') 20 | end 21 | 22 | it "includes the 'action' key:value" do 23 | expect(subject).to include('action:index') 24 | end 25 | 26 | it 'escapes escape sequences as value' do 27 | expect(subject).to include('will_escaped:\\t') 28 | end 29 | 30 | it 'is separated by hard tabs' do 31 | expect(subject.split("\t").count).to eq(payload.count) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/formatters/raw_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Lograge::Formatters::Raw do 4 | it 'serializes custom attributes' do 5 | expect(subject.call(custom: 'data')).to eq(custom: 'data') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/log_subscribers/action_cable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lograge/log_subscribers/action_controller' 4 | require 'active_support' 5 | require 'active_support/notifications' 6 | require 'active_support/core_ext/string' 7 | require 'logger' 8 | require 'active_record' 9 | require 'rails' 10 | 11 | describe Lograge::LogSubscribers::ActionCable do 12 | let(:log_output) { StringIO.new } 13 | let(:logger) do 14 | Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } 15 | end 16 | 17 | let(:subscriber) { Lograge::LogSubscribers::ActionCable.new } 18 | let(:event_params) { { 'foo' => 'bar' } } 19 | 20 | let(:event) do 21 | ActiveSupport::Notifications::Event.new( 22 | 'perform_action.action_cable', 23 | Time.now, 24 | Time.now, 25 | 2, 26 | channel_class: 'ActionCableChannel', 27 | data: event_params, 28 | action: 'pong' 29 | ) 30 | end 31 | 32 | before { Lograge.logger = logger } 33 | 34 | context 'with custom_options configured for cee output' do 35 | before do 36 | Lograge.formatter = ->(data) { "My test: #{data}" } 37 | end 38 | 39 | it 'combines the hash properly for the output' do 40 | Lograge.custom_options = { data: 'value' } 41 | subscriber.perform_action(event) 42 | expect(log_output.string).to match(/^My test: {.*:data=>"value"/) 43 | end 44 | 45 | it 'combines the output of a lambda properly' do 46 | Lograge.custom_options = ->(_event) { { data: 'value' } } 47 | 48 | subscriber.perform_action(event) 49 | expect(log_output.string).to match(/^My test: {.*:data=>"value"/) 50 | end 51 | 52 | it 'works when the method returns nil' do 53 | Lograge.custom_options = ->(_event) {} 54 | 55 | subscriber.perform_action(event) 56 | expect(log_output.string).to be_present 57 | end 58 | end 59 | 60 | context 'when processing an action with lograge output' do 61 | before do 62 | Lograge.formatter = Lograge::Formatters::KeyValue.new 63 | end 64 | 65 | it 'includes the controller and action' do 66 | subscriber.perform_action(event) 67 | expect(log_output.string).to include('controller=ActionCableChannel action=pong') 68 | end 69 | 70 | it 'includes the duration' do 71 | subscriber.perform_action(event) 72 | expect(log_output.string).to match(/duration=[.0-9]{2,4}/) 73 | end 74 | 75 | context 'when an `ActiveRecord::RecordNotFound` is raised' do 76 | let(:exception) { 'ActiveRecord::RecordNotFound' } 77 | 78 | before do 79 | ActionDispatch::ExceptionWrapper.rescue_responses[exception] = :not_found 80 | event.payload[:exception] = [exception, 'Record not found'] 81 | event.payload[:status] = nil 82 | end 83 | 84 | it 'adds a 404 status' do 85 | subscriber.perform_action(event) 86 | expect(log_output.string).to match(/status=404 /) 87 | expect(log_output.string).to match( 88 | /error='ActiveRecord::RecordNotFound: Record not found' / 89 | ) 90 | end 91 | end 92 | 93 | it 'returns a default status when no status or exception is found' do 94 | event.payload[:status] = nil 95 | event.payload[:exception] = nil 96 | subscriber.perform_action(event) 97 | expect(log_output.string).to match(/status=200 /) 98 | end 99 | 100 | it 'does not include a location by default' do 101 | subscriber.perform_action(event) 102 | expect(log_output.string).to_not include('location=') 103 | end 104 | end 105 | 106 | context 'with custom_options configured for lograge output' do 107 | before do 108 | Lograge.formatter = Lograge::Formatters::KeyValue.new 109 | end 110 | 111 | it 'combines the hash properly for the output' do 112 | Lograge.custom_options = { data: 'value' } 113 | subscriber.perform_action(event) 114 | expect(log_output.string).to match(/ data=value/) 115 | end 116 | 117 | it 'combines the output of a lambda properly' do 118 | Lograge.custom_options = ->(_event) { { data: 'value' } } 119 | 120 | subscriber.perform_action(event) 121 | expect(log_output.string).to match(/ data=value/) 122 | end 123 | it 'works when the method returns nil' do 124 | Lograge.custom_options = ->(_event) {} 125 | 126 | subscriber.perform_action(event) 127 | expect(log_output.string).to be_present 128 | end 129 | end 130 | 131 | context 'when event payload includes a "custom_payload"' do 132 | before do 133 | Lograge.formatter = Lograge::Formatters::KeyValue.new 134 | end 135 | 136 | it 'incorporates the payload correctly' do 137 | event.payload[:custom_payload] = { data: 'value' } 138 | 139 | subscriber.perform_action(event) 140 | expect(log_output.string).to match(/ data=value/) 141 | end 142 | 143 | it 'works when custom_payload is nil' do 144 | event.payload[:custom_payload] = nil 145 | 146 | subscriber.perform_action(event) 147 | expect(log_output.string).to be_present 148 | end 149 | end 150 | 151 | context 'with before_format configured for lograge output' do 152 | before do 153 | Lograge.formatter = Lograge::Formatters::KeyValue.new 154 | Lograge.before_format = nil 155 | end 156 | 157 | it 'outputs correctly' do 158 | Lograge.before_format = ->(data, payload) { { status: data[:status] }.merge(action: payload[:action]) } 159 | 160 | subscriber.perform_action(event) 161 | 162 | expect(log_output.string).to include('action=pong') 163 | expect(log_output.string).to include('status=200') 164 | end 165 | it 'works if the method returns nil' do 166 | Lograge.before_format = ->(_data, _payload) {} 167 | 168 | subscriber.perform_action(event) 169 | expect(log_output.string).to be_present 170 | end 171 | end 172 | 173 | context 'with ignore configured' do 174 | before do 175 | Lograge.ignore_nothing 176 | end 177 | 178 | it 'does not log ignored controller actions given a single ignored action' do 179 | Lograge.ignore_actions 'ActionCableChannel#pong' 180 | subscriber.perform_action(event) 181 | expect(log_output.string).to be_blank 182 | end 183 | 184 | it 'does not log ignored controller actions given a single ignored action after a custom ignore' do 185 | Lograge.ignore(->(_event) { false }) 186 | 187 | Lograge.ignore_actions 'ActionCableChannel#pong' 188 | subscriber.perform_action(event) 189 | expect(log_output.string).to be_blank 190 | end 191 | 192 | it 'logs non-ignored controller actions given a single ignored action' do 193 | Lograge.ignore_actions 'ActionCableChannel#bar' 194 | subscriber.perform_action(event) 195 | expect(log_output.string).to be_present 196 | end 197 | 198 | it 'does not log ignored controller actions given multiple ignored actions' do 199 | Lograge.ignore_actions ['ActionCableChannel#bar', 'ActionCableChannel#pong', 'OtherChannel#foo'] 200 | subscriber.perform_action(event) 201 | expect(log_output.string).to be_blank 202 | end 203 | 204 | it 'logs non-ignored controller actions given multiple ignored actions' do 205 | Lograge.ignore_actions ['ActionCableChannel#bar', 'OtherChannel#foo'] 206 | subscriber.perform_action(event) 207 | expect(log_output.string).to_not be_blank 208 | end 209 | 210 | it 'does not log ignored events' do 211 | Lograge.ignore(->(event) { event.payload[:action] == 'pong' }) 212 | 213 | subscriber.perform_action(event) 214 | expect(log_output.string).to be_blank 215 | end 216 | 217 | it 'logs non-ignored events' do 218 | Lograge.ignore(->(event) { event.payload[:action] == 'foo' }) 219 | 220 | subscriber.perform_action(event) 221 | expect(log_output.string).not_to be_blank 222 | end 223 | 224 | it 'does not choke on nil ignore_actions input' do 225 | Lograge.ignore_actions nil 226 | subscriber.perform_action(event) 227 | expect(log_output.string).not_to be_blank 228 | end 229 | 230 | it 'does not choke on nil ignore input' do 231 | Lograge.ignore nil 232 | subscriber.perform_action(event) 233 | expect(log_output.string).not_to be_blank 234 | end 235 | end 236 | 237 | describe 'other actions' do 238 | %i[subscribe unsubscribe connect disconnect].each do |action_name| 239 | let(:event) do 240 | ActiveSupport::Notifications::Event.new( 241 | "#{action_name}.action_cable", 242 | Time.now, 243 | Time.now, 244 | 2, 245 | channel_class: 'ActionCableChannel', 246 | data: event_params, 247 | action: 'pong' 248 | ) 249 | end 250 | 251 | it 'generates output' do 252 | subscriber.perform_action(event) 253 | expect(log_output.string).to include('controller=ActionCableChannel action=pong') 254 | end 255 | end 256 | end 257 | 258 | it "will fallback to ActiveSupport's logger if one isn't configured" do 259 | Lograge.formatter = Lograge::Formatters::KeyValue.new 260 | Lograge.logger = nil 261 | ActiveSupport::LogSubscriber.logger = logger 262 | 263 | subscriber.perform_action(event) 264 | 265 | expect(log_output.string).to be_present 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /spec/log_subscribers/action_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lograge/log_subscribers/action_controller' 4 | require 'active_support' 5 | require 'active_support/notifications' 6 | require 'active_support/core_ext/string' 7 | require 'logger' 8 | require 'active_record' 9 | require 'rails' 10 | 11 | describe Lograge::LogSubscribers::ActionController do 12 | let(:log_output) { StringIO.new } 13 | let(:logger) do 14 | Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } 15 | end 16 | 17 | let(:subscriber) { Lograge::LogSubscribers::ActionController.new } 18 | let(:event_params) { { 'foo' => 'bar' } } 19 | 20 | let(:event) do 21 | ActiveSupport::Notifications::Event.new( 22 | 'process_action.action_controller', 23 | Time.now, 24 | Time.now, 25 | 2, 26 | status: 200, 27 | controller: 'HomeController', 28 | action: 'index', 29 | format: 'application/json', 30 | method: 'GET', 31 | path: '/home?foo=bar', 32 | params: event_params, 33 | db_runtime: 0.02, 34 | view_runtime: 0.01 35 | ) 36 | end 37 | 38 | before { Lograge.logger = logger } 39 | 40 | context 'with custom_options configured for cee output' do 41 | before do 42 | Lograge.formatter = ->(data) { "My test: #{data}" } 43 | end 44 | 45 | it 'combines the hash properly for the output' do 46 | Lograge.custom_options = { data: 'value' } 47 | subscriber.process_action(event) 48 | expect(log_output.string).to match(/^My test: {.*:data=>"value"/) 49 | end 50 | 51 | it 'combines the output of a lambda properly' do 52 | Lograge.custom_options = ->(_event) { { data: 'value' } } 53 | 54 | subscriber.process_action(event) 55 | expect(log_output.string).to match(/^My test: {.*:data=>"value"/) 56 | end 57 | 58 | it 'works when the method returns nil' do 59 | Lograge.custom_options = ->(_event) {} 60 | 61 | subscriber.process_action(event) 62 | expect(log_output.string).to be_present 63 | end 64 | end 65 | 66 | context 'when processing a redirect' do 67 | let(:redirect_event) do 68 | ActiveSupport::Notifications::Event.new( 69 | 'redirect_to.action_controller', 70 | Time.now, 71 | Time.now, 72 | 1, 73 | location: 'http://example.com', 74 | status: 302 75 | ) 76 | end 77 | 78 | it 'stores the location in a thread local variable' do 79 | subscriber.redirect_to(redirect_event) 80 | expect(RequestStore.store[:lograge_location]).to eq('http://example.com') 81 | end 82 | end 83 | 84 | context 'when processing unpermitted parameters' do 85 | let(:unpermitted_parameters_event) do 86 | ActiveSupport::Notifications::Event.new( 87 | 'unpermitted_parameters.action_controller', 88 | Time.now, 89 | Time.now, 90 | 1, 91 | keys: %w[foo bar] 92 | ) 93 | end 94 | 95 | it 'stores the parameters in a thread local variable' do 96 | subscriber.unpermitted_parameters(unpermitted_parameters_event) 97 | expect(RequestStore.store[:lograge_unpermitted_params]).to eq(%w[foo bar]) 98 | end 99 | end 100 | 101 | context 'when processing an action with lograge output' do 102 | before do 103 | Lograge.formatter = Lograge::Formatters::KeyValue.new 104 | end 105 | 106 | it 'includes the URL in the log output' do 107 | subscriber.process_action(event) 108 | expect(log_output.string).to include('/home') 109 | end 110 | 111 | it 'does not include the query string in the url' do 112 | subscriber.process_action(event) 113 | expect(log_output.string).not_to include('?foo=bar') 114 | end 115 | 116 | it 'starts the log line with the HTTP method' do 117 | subscriber.process_action(event) 118 | expect(log_output.string).to match(/^method=GET /) 119 | end 120 | 121 | it 'includes the status code' do 122 | subscriber.process_action(event) 123 | expect(log_output.string).to include('status=200 ') 124 | end 125 | 126 | it 'includes the controller and action' do 127 | subscriber.process_action(event) 128 | expect(log_output.string).to include('controller=HomeController action=index') 129 | end 130 | 131 | it 'includes the duration' do 132 | subscriber.process_action(event) 133 | expect(log_output.string).to match(/duration=[.0-9]{4,4} /) 134 | end 135 | 136 | it 'includes the view rendering time' do 137 | subscriber.process_action(event) 138 | expect(log_output.string).to match(/view=0.01 /) 139 | end 140 | 141 | it 'includes the database rendering time' do 142 | subscriber.process_action(event) 143 | expect(log_output.string).to match(/db=0.02/) 144 | end 145 | 146 | context 'when an `ActiveRecord::RecordNotFound` is raised' do 147 | let(:exception) { 'ActiveRecord::RecordNotFound' } 148 | 149 | before do 150 | ActionDispatch::ExceptionWrapper.rescue_responses[exception] = :not_found 151 | event.payload[:exception] = [exception, 'Record not found'] 152 | event.payload[:status] = nil 153 | end 154 | 155 | it 'adds a 404 status' do 156 | subscriber.process_action(event) 157 | expect(log_output.string).to match(/status=404 /) 158 | expect(log_output.string).to match( 159 | /error='ActiveRecord::RecordNotFound: Record not found' / 160 | ) 161 | end 162 | end 163 | 164 | it 'returns an unknown status when no status or exception is found' do 165 | event.payload[:status] = nil 166 | event.payload[:exception] = nil 167 | subscriber.process_action(event) 168 | expect(log_output.string).to match(/status=0 /) 169 | end 170 | 171 | context 'with a redirect' do 172 | before do 173 | RequestStore.store[:lograge_location] = 'http://www.example.com?key=value' 174 | end 175 | 176 | it 'adds the location to the log line' do 177 | subscriber.process_action(event) 178 | expect(log_output.string).to match(%r{location=http://www.example.com}) 179 | end 180 | 181 | it 'removes the thread local variable' do 182 | subscriber.process_action(event) 183 | expect(RequestStore.store[:lograge_location]).to be_nil 184 | end 185 | end 186 | 187 | it 'does not include a location by default' do 188 | subscriber.process_action(event) 189 | expect(log_output.string).to_not include('location=') 190 | end 191 | 192 | context 'with unpermitted_parameters' do 193 | before do 194 | RequestStore.store[:lograge_unpermitted_params] = %w[florb blarf] 195 | end 196 | 197 | it 'adds the unpermitted_params to the log line' do 198 | subscriber.process_action(event) 199 | expect(log_output.string).to include('unpermitted_params=["florb", "blarf"]') 200 | end 201 | 202 | it 'removes the thread local variable' do 203 | subscriber.process_action(event) 204 | expect(RequestStore.store[:lograge_unpermitted_params]).to be_nil 205 | end 206 | end 207 | 208 | it 'does not include unpermitted_params by default' do 209 | subscriber.process_action(event) 210 | expect(log_output.string).to_not include('unpermitted_params=') 211 | end 212 | 213 | context 'with memory allocations' do 214 | it 'includes allocations when available' do 215 | event_with_allocations = event.dup 216 | event_with_allocations.define_singleton_method :allocations do 217 | 231 218 | end 219 | 220 | subscriber.process_action(event_with_allocations) 221 | expect(log_output.string).to match(/allocations=231/) 222 | end 223 | 224 | it 'fails gracefully when allocations are unavailable' do 225 | event_without_allocations = event.dup 226 | if event_without_allocations.respond_to? :allocations 227 | event_without_allocations.instance_eval('undef :allocations', __FILE__, __LINE__) 228 | end 229 | 230 | subscriber.process_action(event_without_allocations) 231 | expect(log_output.string).to_not match(/allocations=/) 232 | end 233 | end 234 | end 235 | 236 | context 'with custom_options configured for lograge output' do 237 | before do 238 | Lograge.formatter = Lograge::Formatters::KeyValue.new 239 | end 240 | 241 | it 'combines the hash properly for the output' do 242 | Lograge.custom_options = { data: 'value' } 243 | subscriber.process_action(event) 244 | expect(log_output.string).to match(/ data=value/) 245 | end 246 | 247 | it 'combines the output of a lambda properly' do 248 | Lograge.custom_options = ->(_event) { { data: 'value' } } 249 | 250 | subscriber.process_action(event) 251 | expect(log_output.string).to match(/ data=value/) 252 | end 253 | it 'works when the method returns nil' do 254 | Lograge.custom_options = ->(_event) {} 255 | 256 | subscriber.process_action(event) 257 | expect(log_output.string).to be_present 258 | end 259 | end 260 | 261 | context 'when event payload includes a "custom_payload"' do 262 | before do 263 | Lograge.formatter = Lograge::Formatters::KeyValue.new 264 | end 265 | 266 | it 'incorporates the payload correctly' do 267 | event.payload[:custom_payload] = { data: 'value' } 268 | 269 | subscriber.process_action(event) 270 | expect(log_output.string).to match(/ data=value/) 271 | end 272 | 273 | it 'works when custom_payload is nil' do 274 | event.payload[:custom_payload] = nil 275 | 276 | subscriber.process_action(event) 277 | expect(log_output.string).to be_present 278 | end 279 | end 280 | 281 | context 'with before_format configured for lograge output' do 282 | before do 283 | Lograge.formatter = Lograge::Formatters::KeyValue.new 284 | Lograge.before_format = nil 285 | end 286 | 287 | it 'outputs correctly' do 288 | Lograge.before_format = ->(data, payload) { Hash[*data.first].merge(Hash[*payload.first]) } 289 | 290 | subscriber.process_action(event) 291 | 292 | expect(log_output.string).to include('method=GET') 293 | expect(log_output.string).to include('status=200') 294 | end 295 | it 'works if the method returns nil' do 296 | Lograge.before_format = ->(_data, _payload) {} 297 | 298 | subscriber.process_action(event) 299 | expect(log_output.string).to be_present 300 | end 301 | end 302 | 303 | context 'with ignore configured' do 304 | before do 305 | Lograge.ignore_nothing 306 | end 307 | 308 | it 'does not log ignored controller actions given a single ignored action' do 309 | Lograge.ignore_actions 'HomeController#index' 310 | subscriber.process_action(event) 311 | expect(log_output.string).to be_blank 312 | end 313 | 314 | it 'does not log ignored controller actions given a single ignored action after a custom ignore' do 315 | Lograge.ignore(->(_event) { false }) 316 | 317 | Lograge.ignore_actions 'HomeController#index' 318 | subscriber.process_action(event) 319 | expect(log_output.string).to be_blank 320 | end 321 | 322 | it 'logs non-ignored controller actions given a single ignored action' do 323 | Lograge.ignore_actions 'FooController#bar' 324 | subscriber.process_action(event) 325 | expect(log_output.string).to be_present 326 | end 327 | 328 | it 'does not log ignored controller actions given multiple ignored actions' do 329 | Lograge.ignore_actions ['FooController#bar', 'HomeController#index', 'BarController#foo'] 330 | subscriber.process_action(event) 331 | expect(log_output.string).to be_blank 332 | end 333 | 334 | it 'logs non-ignored controller actions given multiple ignored actions' do 335 | Lograge.ignore_actions ['FooController#bar', 'BarController#foo'] 336 | subscriber.process_action(event) 337 | expect(log_output.string).to_not be_blank 338 | end 339 | 340 | it 'does not log ignored events' do 341 | Lograge.ignore(->(event) { event.payload[:method] == 'GET' }) 342 | 343 | subscriber.process_action(event) 344 | expect(log_output.string).to be_blank 345 | end 346 | 347 | it 'logs non-ignored events' do 348 | Lograge.ignore(->(event) { event.payload[:method] == 'foo' }) 349 | 350 | subscriber.process_action(event) 351 | expect(log_output.string).not_to be_blank 352 | end 353 | 354 | it 'does not choke on nil ignore_actions input' do 355 | Lograge.ignore_actions nil 356 | subscriber.process_action(event) 357 | expect(log_output.string).not_to be_blank 358 | end 359 | 360 | it 'does not choke on nil ignore input' do 361 | Lograge.ignore nil 362 | subscriber.process_action(event) 363 | expect(log_output.string).not_to be_blank 364 | end 365 | end 366 | 367 | it "will fallback to ActiveSupport's logger if one isn't configured" do 368 | Lograge.formatter = Lograge::Formatters::KeyValue.new 369 | Lograge.logger = nil 370 | ActiveSupport::LogSubscriber.logger = logger 371 | 372 | subscriber.process_action(event) 373 | 374 | expect(log_output.string).to be_present 375 | end 376 | end 377 | -------------------------------------------------------------------------------- /spec/lograge_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support' 4 | require 'active_support/notifications' 5 | require 'active_support/core_ext/string' 6 | require 'active_support/deprecation' 7 | require 'active_support/log_subscriber' 8 | require 'action_controller' 9 | require 'action_controller/log_subscriber' 10 | require 'action_view/log_subscriber' 11 | 12 | describe Lograge do 13 | context "when removing Rails' log subscribers" do 14 | after do 15 | ActionController::LogSubscriber.attach_to :action_controller 16 | ActionView::LogSubscriber.attach_to :action_view 17 | end 18 | 19 | it 'removes subscribers for controller events' do 20 | expect do 21 | Lograge.remove_existing_log_subscriptions 22 | end.to change { 23 | Lograge.notification_listeners_for('process_action.action_controller') 24 | } 25 | end 26 | 27 | it 'removes subscribers for all events' do 28 | expect do 29 | Lograge.remove_existing_log_subscriptions 30 | end.to change { 31 | Lograge.notification_listeners_for('render_template.action_view') 32 | } 33 | end 34 | 35 | it "does not remove subscribers that aren't from Rails" do 36 | blk = -> {} 37 | ActiveSupport::Notifications.subscribe('process_action.action_controller', &blk) 38 | Lograge.remove_existing_log_subscriptions 39 | listeners = Lograge.notification_listeners_for('process_action.action_controller') 40 | expect(listeners.size).to eq(1) 41 | end 42 | end 43 | 44 | describe 'keep_original_rails_log option' do 45 | context 'when keep_original_rails_log is true' do 46 | let(:app_config) do 47 | double(config: 48 | ActiveSupport::OrderedOptions.new.tap do |config| 49 | config.action_dispatch = double(rack_cache: false) 50 | config.lograge = ActiveSupport::OrderedOptions.new 51 | config.lograge.keep_original_rails_log = true 52 | end) 53 | end 54 | 55 | it "does not remove Rails' subscribers" do 56 | expect(Lograge).to_not receive(:remove_existing_log_subscriptions) 57 | Lograge.setup(app_config) 58 | end 59 | end 60 | end 61 | 62 | describe 'disabling rack_cache verbosity' do 63 | subject { -> { Lograge.setup(app_config) } } 64 | let(:app_config) do 65 | double(config: 66 | ActiveSupport::OrderedOptions.new.tap do |config| 67 | config.action_dispatch = config_option 68 | config.lograge = ActiveSupport::OrderedOptions.new 69 | config.lograge.keep_original_rails_log = true 70 | end) 71 | end 72 | let(:config_option) { double(rack_cache: rack_cache) } 73 | 74 | context 'when rack_cache is false' do 75 | let(:rack_cache) { false } 76 | 77 | it 'does not change config option' do 78 | expect(subject).to_not change { config_option.rack_cache } 79 | end 80 | end 81 | 82 | context 'when rack_cache is a hash' do 83 | let(:rack_cache) { { foo: 'bar', verbose: true } } 84 | 85 | it 'sets verbose to false' do 86 | expect(subject).to change { config_option.rack_cache[:verbose] }.to(false) 87 | end 88 | end 89 | 90 | context 'when rack_cache is true' do 91 | let(:rack_cache) { true } 92 | 93 | it 'does not change config option' do 94 | expect(subject).to_not change { config_option.rack_cache } 95 | end 96 | end 97 | end 98 | 99 | describe 'handling custom_payload option' do 100 | let(:controller_class) { 'ActionController::Base' } 101 | let(:app_config) do 102 | config_obj = ActiveSupport::OrderedOptions.new.tap do |config| 103 | config.action_dispatch = double(rack_cache: false) 104 | config.lograge = Lograge::OrderedOptions.new 105 | config.lograge.custom_payload do |c| 106 | { user_id: c.current_user_id } 107 | end 108 | end 109 | double(config: config_obj) 110 | end 111 | let(:controller) do 112 | Class.new do 113 | def append_info_to_payload(payload) 114 | payload.merge!(appended: true) 115 | end 116 | 117 | def current_user_id 118 | '24601' 119 | end 120 | 121 | class << self 122 | def logger; end 123 | end 124 | end 125 | end 126 | let(:payload) { { timestamp: Date.parse('5-11-1955') } } 127 | 128 | subject { payload.dup } 129 | 130 | before do 131 | stub_const(controller_class, controller) 132 | Lograge.setup(app_config) 133 | controller_class.constantize.new.append_info_to_payload(subject) 134 | end 135 | 136 | it { should eq(payload.merge(appended: true, custom_payload: { user_id: '24601' })) } 137 | 138 | context 'when base_controller_class option is set' do 139 | let(:controller_class) { 'ActionController::API' } 140 | let(:base_controller_class) { controller_class } 141 | let(:app_config) do 142 | config_obj = ActiveSupport::OrderedOptions.new.tap do |config| 143 | config.action_dispatch = double(rack_cache: false) 144 | config.lograge = Lograge::OrderedOptions.new 145 | config.lograge.base_controller_class = base_controller_class 146 | config.lograge.custom_payload do |c| 147 | { user_id: c.current_user_id } 148 | end 149 | end 150 | double(config: config_obj) 151 | end 152 | 153 | it { should eq(payload.merge(appended: true, custom_payload: { user_id: '24601' })) } 154 | 155 | context 'when base_controller_class is an array' do 156 | let(:base_controller_class) { [controller_class] } 157 | 158 | it { should eq(payload.merge(appended: true, custom_payload: { user_id: '24601' })) } 159 | end 160 | end 161 | end 162 | 163 | describe 'deprecated log_format interpreter' do 164 | let(:app_config) do 165 | double(config: 166 | ActiveSupport::OrderedOptions.new.tap do |config| 167 | config.action_dispatch = double(rack_cache: false) 168 | config.lograge = ActiveSupport::OrderedOptions.new 169 | config.lograge.log_format = format 170 | end) 171 | end 172 | before { Lograge.deprecator.silence { Lograge.setup(app_config) } } 173 | subject { Lograge.formatter } 174 | 175 | context ':cee' do 176 | let(:format) { :cee } 177 | 178 | it 'is an instance of Lograge::Formatters::Cee' do 179 | expect(subject).to be_instance_of(Lograge::Formatters::Cee) 180 | end 181 | end 182 | 183 | context ':raw' do 184 | let(:format) { :raw } 185 | 186 | it 'is an instance of Lograge::Formatters::Raw' do 187 | expect(subject).to be_instance_of(Lograge::Formatters::Raw) 188 | end 189 | end 190 | 191 | context ':logstash' do 192 | let(:format) { :logstash } 193 | 194 | it 'is an instance of Lograge::Formatters::Logstash' do 195 | expect(subject).to be_instance_of(Lograge::Formatters::Logstash) 196 | end 197 | end 198 | 199 | context ':graylog2' do 200 | let(:format) { :graylog2 } 201 | 202 | it 'is an instance of Lograge::Formatters::Graylog2' do 203 | expect(subject).to be_instance_of(Lograge::Formatters::Graylog2) 204 | end 205 | end 206 | 207 | context ':lograge' do 208 | let(:format) { :lograge } 209 | 210 | it 'is an instance of Lograge::Formatters::KeyValue' do 211 | expect(subject).to be_instance_of(Lograge::Formatters::KeyValue) 212 | end 213 | end 214 | 215 | context 'default' do 216 | let(:format) { nil } 217 | 218 | it 'is an instance of Lograge::Formatters::KeyValue' do 219 | expect(subject).to be_instance_of(Lograge::Formatters::KeyValue) 220 | end 221 | end 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /spec/silent_logger_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | describe Lograge::SilentLogger do 6 | let(:base_logger) { Logger.new($stdout) } 7 | subject(:silent_logger) { described_class.new(base_logger) } 8 | 9 | it "doesn't call base logger on either log method" do 10 | %i[debug info warn error fatal unknown].each do |method_name| 11 | expect(base_logger).not_to receive(method_name) 12 | 13 | silent_logger.public_send(method_name) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | SimpleCov.start 5 | 6 | # This file was generated by the `rspec --init` command. Conventionally, all 7 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 8 | # Require this file using `require "spec_helper.rb"` to ensure that it is only 9 | # loaded once. 10 | # 11 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 12 | require 'lograge' 13 | require 'action_pack' 14 | require 'support/examples' 15 | 16 | RSpec.configure do |config| 17 | config.run_all_when_everything_filtered = true 18 | config.filter_run :focus 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples_for 'a key value formatter' do 4 | let(:payload) do 5 | { 6 | custom: 'data', 7 | status: 200, 8 | method: 'GET', 9 | path: '/', 10 | controller: 'welcome', 11 | action: 'index' 12 | } 13 | end 14 | 15 | subject { described_class.new.call(payload) } 16 | 17 | it "includes the 'method' key/value" do 18 | expect(subject).to include('method=GET') 19 | end 20 | 21 | it "includes the 'path' key/value" do 22 | expect(subject).to include('path=/') 23 | end 24 | 25 | it "includes the 'status' key/value" do 26 | expect(subject).to include('status=200') 27 | end 28 | 29 | it "includes the 'custom' key/value" do 30 | expect(subject).to include('custom=data') 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tools/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'pathname' 5 | 6 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', File.dirname(__FILE__)) 7 | 8 | require 'rubygems' 9 | require 'bundler/setup' 10 | 11 | require 'pry' 12 | 13 | $LOAD_PATH << 'lib' 14 | require 'lograge' 15 | 16 | Pry::CLI.parse_options 17 | --------------------------------------------------------------------------------