[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non:\n  push:\n    branches: [master]\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        ruby-version: [\"3.0\", \"3.1\", \"3.2\", \"3.3\", \"3.4\", \"4.0\"]\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby-version }}\n          bundler-cache: true\n      - name: Install dependencies\n        run: bundle install --jobs 4 --retry 3\n      - name: Run tests\n        run: bundle exec rake spec\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: \"4.0\"\n          bundler-cache: true\n      - name: Install dependencies\n        run: bundle install\n      - name: Run RuboCop\n        run: bundle exec rake rubocop\n"
  },
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/.yardoc\n/Gemfile.lock\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n.rspec\n.idea/"
  },
  {
    "path": ".rubocop.yml",
    "content": "inherit_from: .rubocop_todo.yml\n\nAllCops:\n  NewCops: enable\n  TargetRubyVersion: 3.0\n\nMetrics/BlockLength:\n  CountAsOne: [array, hash, heredoc, method_call]\n\nMetrics/ClassLength:\n  CountAsOne: [array, hash, heredoc, method_call]\n\nMetrics/MethodLength:\n  CountAsOne: [array, hash, heredoc, method_call]\n  Max: 20\n"
  },
  {
    "path": ".rubocop_todo.yml",
    "content": "# This configuration was generated by\n# `rubocop --auto-gen-config`\n# on 2025-07-10 10:25:51 UTC using RuboCop version 1.77.0.\n# The point is for the user to remove these configuration records\n# one by one as the offenses are removed from the code base.\n# Note that changes in the inspected code, or installation of new\n# versions of RuboCop, may require this file to be generated again.\n\n# Offense count: 3\n# Configuration parameters: EnforcedStyle, AllowedGems, Include.\n# SupportedStyles: Gemfile, gems.rb, gemspec\n# Include: **/*.gemspec, **/Gemfile, **/gems.rb\nGemspec/DevelopmentDependencies:\n  Exclude:\n    - \"grape_logging.gemspec\"\n\n# Offense count: 1\n# This cop supports safe autocorrection (--autocorrect).\n# Configuration parameters: Severity, Include.\n# Include: **/*.gemspec\nGemspec/RequireMFA:\n  Exclude:\n    - \"grape_logging.gemspec\"\n\n# Offense count: 1\n# Configuration parameters: Severity, Include.\n# Include: **/*.gemspec\nGemspec/RequiredRubyVersion:\n  Exclude:\n    - \"grape_logging.gemspec\"\n\n# Offense count: 1\n# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.\nLint/DuplicateBranch:\n  Exclude:\n    - \"lib/grape_logging/util/parameter_filter.rb\"\n\n# Offense count: 1\n# Configuration parameters: AllowedParentClasses.\nLint/MissingSuper:\n  Exclude:\n    - \"lib/grape_logging/loggers/filter_parameters.rb\"\n\n# Offense count: 3\n# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.\nMetrics/AbcSize:\n  Max: 38\n\n# Offense count: 8\n# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.\n# AllowedMethods: refine\nMetrics/BlockLength:\n  Max: 90\n\n# Offense count: 3\n# Configuration parameters: AllowedMethods, AllowedPatterns.\nMetrics/CyclomaticComplexity:\n  Max: 18\n\n# Offense count: 2\n# Configuration parameters: AllowedMethods, AllowedPatterns.\nMetrics/PerceivedComplexity:\n  Max: 19\n\n# Offense count: 2\n# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.\n# SupportedStyles: snake_case, normalcase, non_integer\n# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64\nNaming/VariableNumber:\n  Exclude:\n    - \"spec/lib/grape_logging/formatters/rails_spec.rb\"\n\n# Offense count: 3\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: MinBranchesCount.\nStyle/CaseLikeIf:\n  Exclude:\n    - \"lib/grape_logging/formatters/default.rb\"\n    - \"lib/grape_logging/formatters/logstash.rb\"\n    - \"lib/grape_logging/formatters/rails.rb\"\n\n# Offense count: 17\n# Configuration parameters: AllowedConstants.\nStyle/Documentation:\n  Enabled: false\n\n# Offense count: 29\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: EnforcedStyle.\n# SupportedStyles: always, always_true, never\nStyle/FrozenStringLiteralComment:\n  Enabled: false\n\n# Offense count: 1\n# This cop supports unsafe autocorrection (--autocorrect-all).\nStyle/GlobalStdStream:\n  Exclude:\n    - \"lib/grape_logging/reporters/logger_reporter.rb\"\n\n# Offense count: 1\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: EnforcedStyle.\n# SupportedStyles: literals, strict\nStyle/MutableConstant:\n  Exclude:\n    - \"lib/grape_logging/version.rb\"\n\n# Offense count: 10\nStyle/OpenStructUse:\n  Exclude:\n    - \"spec/lib/grape_logging/loggers/client_env_spec.rb\"\n    - \"spec/lib/grape_logging/loggers/filter_parameters_spec.rb\"\n    - \"spec/lib/grape_logging/loggers/request_headers_spec.rb\"\n    - \"spec/lib/grape_logging/loggers/response_spec.rb\"\n\n# Offense count: 3\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.\n# AllowedMethods: present?, blank?, presence, try, try!\nStyle/SafeNavigation:\n  Exclude:\n    - \"lib/grape_logging/formatters/rails.rb\"\n\n# Offense count: 1\n# This cop supports unsafe autocorrection (--autocorrect-all).\nStyle/SlicingWithRange:\n  Exclude:\n    - \"lib/grape_logging/loggers/request_headers.rb\"\n\n# Offense count: 1\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: RequireEnglish, EnforcedStyle.\n# SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names\nStyle/SpecialGlobalVars:\n  Exclude:\n    - \"spec/spec_helper.rb\"\n\n# Offense count: 3\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: Mode.\nStyle/StringConcatenation:\n  Exclude:\n    - \"lib/grape_logging/formatters/json.rb\"\n    - \"lib/grape_logging/formatters/lograge.rb\"\n    - \"lib/grape_logging/formatters/logstash.rb\"\n\n# Offense count: 1\n# This cop supports unsafe autocorrection (--autocorrect-all).\n# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments.\n# AllowedMethods: define_method\nStyle/SymbolProc:\n  Exclude:\n    - \"lib/grape_logging/util/parameter_filter.rb\"\n\n# Offense count: 11\n# This cop supports safe autocorrection (--autocorrect).\n# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.\n# URISchemes: http, https\nLayout/LineLength:\n  Max: 203\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [3.0.1] - Unreleased\n\n### Changed or Fixed or Added\n\n### Changed\n- Move dev dependencies to Gemfile\n- Use zeitwerk to load gem\n\n[3.0.1]: https://github.com/aserafin/grape_logging/compare/v3.0.0...master\n\n## [3.0.0] - 2025-08-07\n\n### Changed\n- [#93](https://github.com/aserafin/grape_logging/pull/93) RequestLogger middleware to handle Grape 2.4 breaking change - [@devsigner](https://github.com/devsigner) and [@samsonjs](https://github.com/samsonjs).\n\n[3.0.0]: https://github.com/aserafin/grape_logging/compare/v2.1.1...v3.0.0\n\n## [2.1.1] - 2025-07-09\n\n### Fixed\n- [#92](https://github.com/aserafin/grape_logging/pull/92) Handle symbol param keys during filtering - [@samsonjs](https://github.com/samsonjs).\n\n[2.1.1]: https://github.com/aserafin/grape_logging/compare/v2.1.0...v2.1.1\n\n## [2.1.0] - 2025-07-09\n\n### Added\n- [#91](https://github.com/aserafin/grape_logging/pull/91) Add ActionDispatch request ID to logger arguments hash as `:request_id` - [@samsonjs](https://github.com/samsonjs).\n\n[2.1.0]: https://github.com/aserafin/grape_logging/compare/v2.0.0...v2.1.0\n\n## [2.0.0] - 2025-07-09\n\n### Changed\n- **BREAKING**: Updated to support Grape 2.1 and Rack 3.1\n- Minimum supported Ruby version is now 3.0\n- Replaced Travis CI with GitHub Actions for continuous integration\n- Updated all README examples to use `insert_before` instead of `use` for proper middleware placement\n\n### Fixed\n- Fixed LoggerReporter to clone the logger parameter to prevent shared state issues (#77)\n- Fixed view time precision issue by rounding to 2 decimal places\n- Fixed invalid byte sequence handling for parameter keys by properly managing string encodings (#54)\n- Fixed various typos in code comments and spec descriptions (#87)\n- Fixed specs to work with Ruby 3.4's hash inspect format changes\n\n### Documentation\n- Clarified middleware placement requirements in README - must be inserted before Grape::Middleware::Error (#74)\n- Added gem version badge to README\n\n[2.0.0]: https://github.com/aserafin/grape_logging/compare/v1.8.4...v2.0.0\n\n## [1.8.4] - 2021-10-29\n\n### Fixed\n- Rails 6 compatibility improvements\n- Various bug fixes and dependency updates\n\n[1.8.4]: https://github.com/aserafin/grape_logging/compare/v1.8.3...v1.8.4\n\n## [1.8.3] - 2020-02-27\n\n### Fixed\n- Performance improvements\n- Bug fixes for edge cases\n\n[1.8.3]: https://github.com/aserafin/grape_logging/compare/v1.8.2...v1.8.3\n\n## [1.8.2] - 2019-10-08\n\n### Fixed\n- Thread safety improvements\n- Minor bug fixes\n\nNote: This version was tagged as \"v.1.8.2\" (with extra dot)\n\n[1.8.2]: https://github.com/aserafin/grape_logging/compare/v1.8.1...v.1.8.2\n\n## [1.8.1] - 2019-08-07\n\n### Fixed\n- Bug fixes for parameter filtering\n- Improved error handling\n\n[1.8.1]: https://github.com/aserafin/grape_logging/compare/v1.8.0...v1.8.1\n\n## [1.8.0] - 2019-05-30\n\n### Added\n- Rails formatter for better Rails integration\n- Improved Rails instrumentation support\n\n[1.8.0]: https://github.com/aserafin/grape_logging/compare/v1.7.0...v1.8.0\n\n## [1.7.0] - 2017-11-09\n\n### Added\n- Logstash formatter for ELK stack integration\n- Enhanced JSON formatting options\n\n[1.7.0]: https://github.com/aserafin/grape_logging/compare/v1.6.0...v1.7.0\n\n## [1.6.0] - 2017-07-20\n\n### Added\n- MultiIO support for logging to multiple destinations simultaneously\n- Can now log to both file and STDOUT\n\n[1.6.0]: https://github.com/aserafin/grape_logging/compare/v1.5.0...v1.6.0\n\n## [1.5.0] - 2017-06-15\n\n### Added\n- Configurable log levels\n- Better control over logging verbosity\n\n[1.5.0]: https://github.com/aserafin/grape_logging/compare/v1.4.0...v1.5.0\n\n## [1.4.0] - 2017-01-12\n\n### Added\n- FilterParameters logger for sensitive parameter filtering\n- Automatic Rails filter_parameters integration when available\n\n[1.4.0]: https://github.com/aserafin/grape_logging/compare/v1.3.0...v1.4.0\n\n## [1.3.0] - 2016-12-08\n\n### Added\n- RequestHeaders logger for logging HTTP request headers\n- ClientEnv logger for logging client IP and user agent\n\n[1.3.0]: https://github.com/aserafin/grape_logging/compare/v1.2.1...v1.3.0\n\n## [1.2.1] - 2016-04-14\n\n### Added\n- JSON formatter for structured logging\n- Rails instrumentation support via ActiveSupport::Notifications\n\n### Fixed\n- Parameter handling improvements\n\n[1.2.1]: https://github.com/aserafin/grape_logging/compare/v1.2.0...v1.2.1\n\n## [1.2.0] - 2016-01-21\n\n### Added\n- Response logger for logging response details\n- Improved parameter logging\n\n### Changed\n- Better integration with Grape middleware stack\n\n[1.2.0]: https://github.com/aserafin/grape_logging/compare/v1.1.3...v1.2.0\n\n## [1.1.3] - 2015-12-03\n\n### Fixed\n- Bug fixes for Grape 0.14 compatibility\n- Improved error handling\n\n[1.1.3]: https://github.com/aserafin/grape_logging/compare/v1.1.2...v1.1.3\n\n## [1.1.2] - 2015-11-19\n\n### Fixed\n- Performance optimizations\n- Minor bug fixes\n\n[1.1.2]: https://github.com/aserafin/grape_logging/compare/v1.1.1...v1.1.2\n\n## [1.1.1] - 2015-11-12\n\n### Fixed\n- Critical bug fix for middleware initialization\n\nNote: This version was tagged as \"v.1.1.1\" (with extra dot)\n\n[1.1.1]: https://github.com/aserafin/grape_logging/compare/v1.1.0...v.1.1.1\n\n## [1.1.0] - 2015-11-09\n\n### Added\n- Pluggable logger architecture\n- Support for custom loggers via include option\n- Base logger class for extending functionality\n\n### Changed\n- Refactored middleware for better extensibility\n\n[1.1.0]: https://github.com/aserafin/grape_logging/compare/v1.0.3...v1.1.0\n\n## [1.0.3] - 2015-11-05\n\n### Fixed\n- Compatibility fixes for different Grape versions\n- Bug fixes\n\n[1.0.3]: https://github.com/aserafin/grape_logging/compare/v1.0.2...v1.0.3\n\n## [1.0.2] - 2015-10-29\n\n### Fixed\n- Minor bug fixes and improvements\n\n[1.0.2]: https://github.com/aserafin/grape_logging/compare/v1.0.1...v1.0.2\n\n## [1.0.1] - 2015-10-22\n\n### Fixed\n- Initial bug fixes after 1.0 release\n\n[1.0.1]: https://github.com/aserafin/grape_logging/compare/v1.0...v1.0.1\n\n## [1.0] - 2015-10-15\n\n### Added\n- Initial release\n- Request logging middleware for Grape APIs\n- Basic request/response logging\n- Configurable formatters\n- Time tracking (total, db, view)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to grape_logging\n\nThis project is work of many contributors. You're encouraged to submit pull requests, propose features and discuss issues.\n\n## Fork the Project\n\nFork the project on Github and check out your copy.\n\n```\ngit clone https://github.com/contributor/grape_logging.git\ncd grape_logging\ngit remote add upstream https://github.com/aserafin/grape_logging.git\n```\n\n## Create a Topic Branch\n\nMake sure your fork is up-to-date and create a topic branch for your feature or bug fix.\n\n```\ngit checkout master\ngit pull upstream master\ngit checkout -b my-feature-branch\n```\n\n## Bundle Install and Test\n\nEnsure that you can build the project and run tests.\n\n```\nbundle install\nbundle exec rake\n```\n\n## Write Tests\n\nTry to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to the spec directory.\n\n## Write Code\n\nImplement your feature or bug fix.\n\nRuby style is enforced with RuboCop. Run `bundle exec rubocop` and fix any style issues highlighted.\n\nMake sure that `bundle exec rake` completes without errors.\n\n## Write Documentation\n\nDocument any external behavior in the README.md.\n\n## Update Changelog\n\nAdd a line to Changelog.md under *Next Release*. Make it look like every other line, including your name and link to your Github account.\n\n## Commit Changes\n\nMake sure git knows your name and email address:\n\n```\ngit config --global user.name \"Your Name\"\ngit config --global user.email \"contributor@example.com\"\n```\n\nWriting good commit logs is important. A commit log should describe what changed and why.\n\n```\ngit add ...\ngit commit\n```\n\n## Push\n\n```\ngit push origin my-feature-branch\n```\n\n## Make a Pull Request\n\nGo to https://github.com/contributor/grape_logging and select your feature branch. Click the 'Pull Request' button and fill out the form.\n\nWe'll try to review pull requests within a few days but as this is maintained by a small group of volunteers there is no guarantee that we'll look at it within any time frame, or at all. There's no maintenance guarantee.\n\n## Discuss and Update\n\nYou may get feedback or requests for changes to your pull request. This is a big part of the submission process so don't be discouraged!\n\nSome things that will increase the chance that your pull request is accepted:\n\n- Write tests.\n- Follow the Ruby style guide.\n- Write a good commit message.\n\nIf you'd like to discuss a feature or bug fix before starting work, please [create an issue](https://github.com/aserafin/grape_logging/issues) first. This helps ensure your contribution aligns with the project's direction and avoids duplicate efforts.\n\n## Rebase\n\nIf you've been working on a change for a while, rebase with upstream/master.\n\n```\ngit fetch upstream\ngit rebase upstream/master\ngit push origin my-feature-branch -f\n```\n\n## Be Patient\n\nIt's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang in there!\n\n## Thank You\n\nPlease do know that we really appreciate and value your time and work. We love you, really.\n"
  },
  {
    "path": "Gemfile",
    "content": "source 'https://rubygems.org'\n\n# Specify your gem's dependencies in grape_logging.gemspec\ngemspec\n\ngem 'rake', '~> 13.3'\ngem 'rspec', '~> 3.5'\n\n# This is pinned to an exact version otherwise we can't know which rules\n# are in play at any given time in different environments.\ngem 'rubocop', '1.77.0'\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 aserafin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# grape_logging\n\n[![Gem Version](https://badge.fury.io/rb/grape_logging.svg)](https://badge.fury.io/rb/grape_logging)\n[![CI](https://github.com/aserafin/grape_logging/actions/workflows/ci.yml/badge.svg)](https://github.com/aserafin/grape_logging/actions/workflows/ci.yml)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'grape_logging'\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install grape_logging\n\n## Basic Usage\n\nIn your API file (somewhere on the top), insert grape logging middleware before grape error middleware. This is important due to the behaviour of `lib/grape/middleware/error.rb`, which manipulates the status of the response when there is an error.\n\n```ruby\nrequire 'grape_logging'\nlogger.formatter = GrapeLogging::Formatters::Default.new\ninsert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, { logger: logger }\n```\n\n**ProTip:** If your logger doesn't support setting formatter you can remove this line - it's optional\n\n## Features\n\n### Log Format\n\nThere are formatters provided for you, or you can provide your own.\n\n#### `GrapeLogging::Formatters::Default`\n\n    [2015-04-16 12:52:12 +0200] INFO -- 200 -- total=2.06 db=0.36 -- PATCH /api/endpoint params={\"some_param\"=>{\"value_1\"=>\"123\", \"value_2\"=>\"456\"}}\n\n#### `GrapeLogging::Formatters::Json`\n\n```json\n{\n  \"date\": \"2015-04-16 12:52:12+0200\",\n  \"severity\": \"INFO\",\n  \"data\": {\n    \"status\": 200,\n    \"time\": {\n      \"total\": 2.06,\n      \"db\": 0.36,\n      \"view\": 1.70\n    },\n    \"method\": \"PATCH\",\n    \"path\": \"/api/endpoint\",\n    \"params\": {\n      \"value_1\": \"123\",\n      \"value_2\": \"456\"\n    },\n    \"host\": \"localhost\"\n  }\n}\n```\n\n#### `GrapeLogging::Formatters::Lograge`\n\n    severity=\"INFO\", duration=2.06, db=0.36, view=1.70, datetime=\"2015-04-16 12:52:12+0200\", status=200, method=\"PATCH\", path=\"/api/endpoint\", params={}, host=\"localhost\"\n\n#### `GrapeLogging::Formatters::Logstash`\n\n```json\n{\n  \"@timestamp\": \"2015-04-16 12:52:12+0200\",\n  \"severity\": \"INFO\",\n  \"status\": 200,\n  \"time\": {\n    \"total\": 2.06,\n    \"db\": 0.36,\n    \"view\": 1.70\n  },\n  \"method\": \"PATCH\",\n  \"path\": \"/api/endpoint\",\n  \"params\": {\n    \"value_1\": \"123\",\n    \"value_2\": \"456\"\n  },\n  \"host\": \"localhost\"\n}\n```\n\n#### `GrapeLogging::Formatters::Rails`\n\nRails will print the \"Started...\" line:\n\n    Started GET \"/api/endpoint\" for ::1 at 2015-04-16 12:52:12 +0200\n      User Load (0.7ms)  SELECT \"users\".* FROM \"users\" WHERE  \"users\".\"id\" = $1\n      ...\n\nThe `Rails` formatter adds the last line of the request, like a standard Rails request:\n\n    Completed 200 OK in 349ms (Views: 250.1ms | DB: 98.63ms)\n\n#### Custom\n\nYou can provide your own class that implements the `call` method returning a `String`:\n\n```ruby\ndef call(severity, datetime, _, data)\n   ...\nend\n```\n\nYou can change the formatter like so\n```ruby\nclass MyAPI < Grape::API\n  insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, logger: logger, formatter: MyFormatter.new\nend\n```\n\nIf you prefer some other format I strongly encourage you to do pull request with new formatter class ;)\n\n### Customising What Is Logged\n\nYou can include logging of other parts of the request / response cycle by including subclasses of `GrapeLogging::Loggers::Base`\n```ruby\nclass MyAPI < Grape::API\n  insert_before Grape::Middleware::Error,\n    GrapeLogging::Middleware::RequestLogger,\n    logger: logger,\n    include: [ GrapeLogging::Loggers::Response.new,\n               GrapeLogging::Loggers::FilterParameters.new,\n               GrapeLogging::Loggers::ClientEnv.new,\n               GrapeLogging::Loggers::RequestHeaders.new ]\nend\n```\n\n#### FilterParameters\nThe `FilterParameters` logger will filter out sensitive parameters from your logs. If mounted inside rails, will use the `Rails.application.config.filter_parameters` by default. Otherwise, you must specify a list of keys to filter out.\n\n#### ClientEnv\nThe `ClientEnv` logger will add `ip` and user agent `ua` in your log.\n\n#### RequestHeaders\nThe `RequestHeaders` logger will add `request headers` in your log.\n\n### Logging to file and STDOUT\n\nYou can log to file and STDOUT at the same time, you just need to assign new logger\n```ruby\nlog_file = File.open('path/to/your/logfile.log', 'a')\nlog_file.sync = true\nlogger Logger.new GrapeLogging::MultiIO.new(STDOUT, log_file)\n```\n\n### Set the log level\n\nYou can control the level used to log. The default is `info`.\n\n```ruby\nclass MyAPI < Grape::API\n  insert_before Grape::Middleware::Error,\n    GrapeLogging::Middleware::RequestLogger,\n    logger: logger,\n    log_level: 'debug'\nend\n```\n\n### Logging via Rails instrumentation\n\nYou can choose to not pass the logger to ```grape_logging``` but instead send logs to Rails instrumentation in order to let Rails and its configured Logger do the log job, for example.\nFirst, config ```grape_logging```, like that:\n```ruby\nclass MyAPI < Grape::API\n  insert_before Grape::Middleware::Error,\n    GrapeLogging::Middleware::RequestLogger,\n    instrumentation_key: 'grape_key',\n    include: [ GrapeLogging::Loggers::Response.new,\n               GrapeLogging::Loggers::FilterParameters.new ]\nend\n```\n\nand then add an initializer in your Rails project:\n```ruby\n# config/initializers/instrumentation.rb\n\n# Subscribe to grape request and log with Rails.logger\nActiveSupport::Notifications.subscribe('grape_key') do |name, starts, ends, notification_id, payload|\n  Rails.logger.info payload\nend\n```\n\nThe idea come from here: https://gist.github.com/teamon/e8ae16ffb0cb447e5b49\n\n### Logging exceptions\n\nIf you want to log exceptions you can do it like this\n```ruby\nclass MyAPI < Grape::API\n  rescue_from :all do |e|\n    MyAPI.logger.error e\n    #do here whatever you originally planned to do :)\n  end\nend\n```\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\nFor maintainers releasing a new version, please see [RELEASING.md](RELEASING.md).\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Releasing grape_logging\n\nThere're no particular rules about when to release grape_logging. Release bug fixes frequently, features not so frequently and breaking API changes rarely.\n\n### Pre-flight Checks\n\nRun tests, check that all tests succeed locally.\n\n```\nbundle install\nrake\n```\n\nCheck that the last build succeeded in [GitHub Actions](https://github.com/aserafin/grape_logging/actions) for all supported platforms.\n\n### Update Changelog\n\nChange \"Unreleased\" in [CHANGELOG.md](https://github.com/aserafin/grape_logging/blob/master/CHANGELOG.md) to the new version and date:\n\n```\n## [1.8.5] - 2024-06-28\n\n### Changed\n- Description of changes\n\n### Fixed\n- Description of fixes\n\n### Added\n- Description of additions\n\n[1.8.5]: https://github.com/aserafin/grape_logging/compare/v1.8.4...v1.8.5\n```\n\nRemove the line with \"Your contribution here.\", since there will be no more contributions to this release.\n\nOnly include the sections (Changed, Fixed, Added, etc.) that have actual changes.\n\nCommit your changes.\n\n```shell\ngit add CHANGELOG.md lib/grape_logging/version.rb\ngit commit -m \"Preparing for release, 1.8.5.\"\ngit push\n```\n\n### Release on RubyGems and GitHub\n\n#### Option 1: Automated (Recommended)\n\nUse the combined task that releases the gem and creates a GitHub release:\n\n```shell\nrake github_release\n```\n\nThis will:\n1. Build and push the gem to RubyGems\n2. Create and push the git tag\n3. Create a GitHub release with auto-generated changelog\n\n#### Option 2: Manual\n\nFirst, release the gem:\n\n```shell\nrake release\n```\n\nOutput will look something like:\n```\ngrape_logging 1.8.5 built to pkg/grape_logging-1.8.5.gem.\nTagged v1.8.5.\nPushed git commits and tags.\nPushed grape_logging 1.8.5 to rubygems.org.\n```\n\nThen create the GitHub release on the web or using `gh`:\n\n```\ngh release create v1.8.5 --generate-notes --verify-tag\n```\n\nThis uses GitHub's automatic changelog generation feature to create release notes from merged pull requests and commits since the last release.\n\n### Prepare for the Next Version\n\nModify `lib/grape_logging/version.rb`, increment the version number (eg. change `1.8.5` to `1.8.6`).\n\n```ruby\nmodule GrapeLogging\n  VERSION = '1.8.6'.freeze\nend\n```\n\nAdd the next release to [CHANGELOG.md](https://github.com/aserafin/grape_logging/blob/master/CHANGELOG.md).\n\n```\n## [1.8.6] - Unreleased\n\n### Changed or Fixed or Added\n- Your contribution here.\n\n[1.8.6]: https://github.com/aserafin/grape_logging/compare/v1.8.5...master\n```\n\nCommit your changes.\n\n```\ngit add CHANGELOG.md lib/grape_logging/version.rb\ngit commit -m \"Bump version to 1.8.6.\"\ngit push\n```\n"
  },
  {
    "path": "Rakefile",
    "content": "require 'bundler/gem_tasks'\nrequire 'rspec/core/rake_task'\nrequire 'rubocop/rake_task'\n\nRSpec::Core::RakeTask.new(:spec) do |spec|\n  spec.rspec_opts = ['-fd -c']\n  spec.pattern = FileList['spec/**/*_spec.rb']\nend\n\nRuboCop::RakeTask.new(:rubocop) do |t|\n  t.patterns = ['lib/**/*.rb', 'spec/**/*.rb', 'Rakefile', 'Gemfile', 'grape_logging.gemspec']\nend\n\ntask default: %i[spec rubocop]\n\ndesc 'Release gem and create GitHub release'\ntask github_release: :release do\n  require 'grape_logging/version'\n\n  version = \"v#{GrapeLogging::VERSION}\"\n\n  # Check if gh CLI is available\n  unless system('which gh > /dev/null 2>&1')\n    puts \"\\n⚠️  GitHub CLI (gh) not found\"\n    puts 'To create a GitHub release with auto-generated changelog, install gh:'\n    puts '  brew install gh  # macOS with Homebrew'\n    puts '  # or visit: https://github.com/cli/cli#installation'\n    puts \"\\nYou can manually create the release with:\"\n    puts \"  gh release create v#{GrapeLogging::VERSION} --generate-notes\"\n    next\n  end\n\n  # Create GitHub release\n  puts \"\\nCreating GitHub release #{version}...\"\n\n  if system('gh', 'release', 'create', version, '--generate-notes', '--verify-tag')\n    puts \"✅ GitHub release #{version} created successfully\"\n  else\n    puts '❌ Failed to create GitHub release'\n    puts 'You can manually create it with:'\n    puts \"  gh release create '#{version}' --generate-notes --verify-tag\"\n  end\nend\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n\nrequire 'bundler/setup'\nrequire 'grape_logging'\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire 'irb'\nIRB.start\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/bin/bash\nset -euo pipefail\nIFS=$'\\n\\t'\n\nbundle install\n\n# Do any other automated setup that you need to do here\n"
  },
  {
    "path": "grape_logging.gemspec",
    "content": "lib = File.expand_path('lib', __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire 'grape_logging/version'\n\nGem::Specification.new do |spec|\n  spec.name          = 'grape_logging'\n  spec.version       = GrapeLogging::VERSION\n  spec.authors       = ['aserafin', 'Sami Samhuri']\n  spec.email         = ['adrian@softmad.pl', 'sami@samhuri.net']\n\n  spec.summary       = 'Out of the box request logging for Grape!'\n  spec.description   = 'This gem provides simple request logging for Grape with just few lines ' \\\n                       'of code you have to put in your project! In return you will get response ' \\\n                       'codes, paths, parameters and more!'\n  spec.homepage      = 'http://github.com/aserafin/grape_logging'\n  spec.license       = 'MIT'\n\n  spec.files         = `git ls-files -z`.split(\"\\x0\").reject { |f| f.match(%r{^(test|spec|features)/}) }\n  spec.bindir        = 'exe'\n  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }\n  spec.require_paths = ['lib']\n\n  spec.add_dependency 'grape', '>= 2.4.0'\n  spec.add_dependency 'rack'\n  spec.add_dependency 'zeitwerk'\nend\n"
  },
  {
    "path": "lib/grape_logging/formatters/default.rb",
    "content": "module GrapeLogging\n  module Formatters\n    class Default\n      def call(severity, datetime, _, data)\n        \"[#{datetime}] #{severity} -- #{format(data)}\\n\"\n      end\n\n      def format(data)\n        if data.is_a?(String)\n          data\n        elsif data.is_a?(Exception)\n          format_exception(data)\n        elsif data.is_a?(Hash)\n          \"#{data.delete(:status)} -- #{format_hash(data.delete(:time))} -- #{data.delete(:method)} #{data.delete(:path)} #{format_hash(data)}\"\n        else\n          data.inspect\n        end\n      end\n\n      private\n\n      def format_hash(hash)\n        hash.keys.sort.map { |key| \"#{key}=#{hash[key]}\" }.join(' ')\n      end\n\n      def format_exception(exception)\n        backtrace_array = (exception.backtrace || []).map { |line| \"\\t#{line}\" }\n        \"#{exception.message}\\n#{backtrace_array.join(\"\\n\")}\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/formatters/json.rb",
    "content": "module GrapeLogging\n  module Formatters\n    class Json\n      def call(severity, datetime, _, data)\n        {\n          date: datetime,\n          severity: severity,\n          data: format(data)\n        }.to_json + \"\\n\"\n      end\n\n      private\n\n      def format(data)\n        if data.is_a?(String) || data.is_a?(Hash)\n          data\n        elsif data.is_a?(Exception)\n          format_exception(data)\n        else\n          data.inspect\n        end\n      end\n\n      def format_exception(exception)\n        {\n          exception: {\n            message: exception.message\n          }\n        }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/formatters/lograge.rb",
    "content": "module GrapeLogging\n  module Formatters\n    class Lograge\n      def call(severity, datetime, _, data)\n        time = data.delete :time\n        attributes = {\n          severity: severity,\n          duration: time[:total],\n          db: time[:db],\n          view: time[:view],\n          datetime: datetime.iso8601\n        }.merge(data)\n        ::Lograge.formatter.call(attributes) + \"\\n\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/formatters/logstash.rb",
    "content": "module GrapeLogging\n  module Formatters\n    class Logstash\n      def call(severity, datetime, _, data)\n        {\n          '@timestamp': datetime.iso8601,\n          '@version': '1',\n          severity: severity\n        }.merge!(format(data)).to_json + \"\\n\"\n      end\n\n      private\n\n      def format(data)\n        if data.is_a?(Hash)\n          data\n        elsif data.is_a?(String)\n          { message: data }\n        elsif data.is_a?(Exception)\n          format_exception(data)\n        else\n          { message: data.inspect }\n        end\n      end\n\n      def format_exception(exception)\n        {\n          exception: {\n            message: exception.message\n          }\n        }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/formatters/rails.rb",
    "content": "module GrapeLogging\n  module Formatters\n    class Rails\n      def call(severity, datetime, _, data)\n        if data.is_a?(String)\n          \"#{severity[0..0]} [#{datetime}] #{severity} -- : #{data}\\n\"\n        elsif data.is_a?(Exception)\n          \"#{severity[0..0]} [#{datetime}] #{severity} -- : #{format_exception(data)}\\n\"\n        elsif data.is_a?(Hash)\n          format_hash(data)\n        else\n          \"#{data.inspect}\\n\"\n        end\n      end\n\n      private\n\n      def format_exception(exception)\n        backtrace_array = (exception.backtrace || []).map { |line| \"\\t#{line}\" }\n\n        [\n          \"#{exception.message} (#{exception.class})\",\n          backtrace_array.join(\"\\n\")\n        ].reject { |line| line == '' }.join(\"\\n\")\n      end\n\n      def format_hash(hash)\n        # Create Rails' single summary line at the end of every request, formatted like:\n        # Completed 200 OK in 958ms (Views: 951.1ms | ActiveRecord: 3.8ms)\n        # See: actionpack/lib/action_controller/log_subscriber.rb\n\n        message = ''\n        additions = []\n        status = hash.delete(:status)\n        params = hash.delete(:params)\n\n        total_time = hash[:time] && hash[:time][:total] && hash[:time][:total].round(2)\n        view_time  = hash[:time] && hash[:time][:view]  && hash[:time][:view].round(2)\n        db_time    = hash[:time] && hash[:time][:db]    && hash[:time][:db].round(2)\n\n        additions << \"Views: #{view_time}ms\" if view_time\n        additions << \"DB: #{db_time}ms\"      if db_time\n\n        message << \"  Parameters: #{params.inspect}\\n\" if params\n\n        message << \"Completed #{status} #{::Rack::Utils::HTTP_STATUS_CODES[status]} in #{total_time}ms\"\n        message << \" (#{additions.join(' | '.freeze)})\" unless additions.empty?\n        message << \"\\n\"\n        message << \"\\n\" if defined?(::Rails.env) && ::Rails.env.development?\n\n        message\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/loggers/base.rb",
    "content": "module GrapeLogging\n  module Loggers\n    class Base\n      def parameters(_request, _response)\n        {}\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/loggers/client_env.rb",
    "content": "module GrapeLogging\n  module Loggers\n    class ClientEnv < GrapeLogging::Loggers::Base\n      def parameters(request, _)\n        { ip: request.env['HTTP_X_FORWARDED_FOR'] || request.env['REMOTE_ADDR'], ua: request.env['HTTP_USER_AGENT'] }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/loggers/filter_parameters.rb",
    "content": "module GrapeLogging\n  module Loggers\n    class FilterParameters < GrapeLogging::Loggers::Base\n      AD_PARAMS = 'action_dispatch.request.parameters'.freeze\n\n      def initialize(filter_parameters = nil, replacement = nil, exceptions = %w[controller action format])\n        @filter_parameters = filter_parameters || (defined?(::Rails.application) ? ::Rails.application.config.filter_parameters : [])\n        @replacement = replacement || '[FILTERED]'\n        @exceptions = exceptions\n      end\n\n      def parameters(request, _)\n        { params: safe_parameters(request) }\n      end\n\n      private\n\n      def parameter_filter\n        @parameter_filter ||= GrapeLogging::Util::ParameterFilter.new(@replacement, @filter_parameters)\n      end\n\n      def safe_parameters(request)\n        # Now this logger can work also over Rails requests\n        if request.params.empty?\n          clean_parameters(request.env[AD_PARAMS] || {})\n        else\n          clean_parameters(request.params)\n        end\n      end\n\n      def clean_parameters(parameters)\n        original_encoding_map = build_encoding_map(parameters)\n        params = transform_key_encoding(parameters, Hash.new { |h, _| [Encoding::ASCII_8BIT, h] })\n        cleaned_params = parameter_filter.filter(params).except(*@exceptions)\n        transform_key_encoding(cleaned_params, original_encoding_map)\n      end\n\n      def build_encoding_map(parameters)\n        parameters.each_with_object({}) do |(k, v), h|\n          key_str = k.to_s\n          h[key_str.dup.force_encoding(Encoding::ASCII_8BIT)] = [key_str.encoding, v.is_a?(Hash) ? build_encoding_map(v) : nil]\n        end\n      end\n\n      def transform_key_encoding(parameters, encoding_map)\n        parameters.each_with_object({}) do |(k, v), h|\n          key_str = k.to_s\n          encoding, children_encoding_map = encoding_map[key_str]\n          h[key_str.dup.force_encoding(encoding)] = v.is_a?(Hash) ? transform_key_encoding(v, children_encoding_map) : v\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/loggers/request_headers.rb",
    "content": "module GrapeLogging\n  module Loggers\n    class RequestHeaders < GrapeLogging::Loggers::Base\n      HTTP_PREFIX = 'HTTP_'.freeze\n\n      def parameters(request, _)\n        headers = {}\n\n        request.env.each_pair do |k, v|\n          next unless k.to_s.start_with? HTTP_PREFIX\n\n          k = k[5..-1].split('_').each(&:capitalize!).join('-')\n          headers[k] = v\n        end\n\n        { headers: headers }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/loggers/response.rb",
    "content": "module GrapeLogging\n  module Loggers\n    class Response < GrapeLogging::Loggers::Base\n      def parameters(_, response)\n        response ? { response: serialized_response_body(response) } : {}\n      end\n\n      private\n\n      # In some cases, response.body is not parseable by JSON.\n      # For example, if you POST on a PUT endpoint, response.body is egal to \"\"\"\".\n      # It's strange, but it's the Grape behavior...\n      def serialized_response_body(response)\n        if response.respond_to?(:body)\n          # Rack responses\n          begin\n            response.body.map { |body| JSON.parse(body.to_s) }\n          rescue StandardError # No reason to have \"=> e\" here when we don't use it..\n            response.body\n          end\n        else\n          # Error & Exception responses\n          response\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/middleware/request_logger.rb",
    "content": "module GrapeLogging\n  module Middleware\n    class RequestLogger < Grape::Middleware::Base\n      if defined?(ActiveRecord)\n        ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|\n          event = ActiveSupport::Notifications::Event.new(*args)\n          GrapeLogging::Timings.append_db_runtime(event)\n        end\n      end\n\n      # Persist response status & response (body)\n      # to use int in parameters\n      attr_accessor :response_status, :response_body\n\n      def initialize(app, **options)\n        super\n\n        @included_loggers = @options[:include] || []\n        @reporter =\n          if options[:instrumentation_key]\n            Reporters::ActiveSupportReporter.new(@options[:instrumentation_key])\n          else\n            Reporters::LoggerReporter.new(@options[:logger], @options[:formatter], @options[:log_level])\n          end\n      end\n\n      def before\n        reset_db_runtime\n        start_time\n        invoke_included_loggers(:before)\n      end\n\n      def after(status, response)\n        stop_time\n\n        # Response status\n        @response_status = status\n        @response_body   = response\n\n        # Perform reporters\n        @reporter.perform(collect_parameters)\n\n        # Invoke loggers\n        invoke_included_loggers(:after)\n        nil\n      end\n\n      # Call stack and parse responses & status.\n      #\n      # @note Exceptions are logged as 500 status & re-raised.\n      def call!(env)\n        @env = env\n\n        # Before hook\n        before\n\n        # Catch error\n        error = catch(:error) do\n          @app_response = @app.call(@env)\n          nil\n        rescue StandardError => e\n          # Log as 500 + message\n          after(e.respond_to?(:status) ? e.status : 500, e.message)\n\n          # Re-raise exception\n          raise e\n        end\n\n        # Get status & response from app_response\n        # when no error occurs.\n        if error\n          # Call with error & response\n          after(error[:status], error[:message])\n\n          # Throw again\n          throw(:error, error)\n        else\n          status, _, resp = *@app_response\n\n          # Call after hook properly\n          after(status, resp)\n        end\n\n        # Otherwise return original response\n        @app_response\n      end\n\n      protected\n\n      def parameters\n        {\n          status: response_status,\n          time: {\n            total: total_runtime,\n            db: db_runtime,\n            view: view_runtime\n          },\n          method: request.request_method,\n          path: request.path,\n          params: request.params,\n          host: request.host,\n          request_id: env['action_dispatch.request_id']\n        }\n      end\n\n      private\n\n      def request\n        @request ||= ::Rack::Request.new(@env)\n      end\n\n      def total_runtime\n        ((stop_time - start_time) * 1000).round(2)\n      end\n\n      def view_runtime\n        (total_runtime - db_runtime).round(2)\n      end\n\n      def db_runtime\n        GrapeLogging::Timings.db_runtime.round(2)\n      end\n\n      def reset_db_runtime\n        GrapeLogging::Timings.reset_db_runtime\n      end\n\n      def start_time\n        @start_time ||= Time.now\n      end\n\n      def stop_time\n        @stop_time ||= Time.now\n      end\n\n      def collect_parameters\n        parameters.tap do |params|\n          @included_loggers.each do |logger|\n            params.merge! logger.parameters(request, response_body) do |_, oldval, newval|\n              oldval.respond_to?(:merge) ? oldval.merge(newval) : newval\n            end\n          end\n        end\n      end\n\n      def invoke_included_loggers(method_name)\n        @included_loggers.each do |logger|\n          logger.send(method_name) if logger.respond_to?(method_name)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/multi_io.rb",
    "content": "module GrapeLogging\n  class MultiIO\n    def initialize(*targets)\n      @targets = targets\n    end\n\n    def write(*args)\n      @targets.each { |t| t.write(*args) }\n    end\n\n    def close\n      @targets.each(&:close)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/reporters/active_support_reporter.rb",
    "content": "module GrapeLogging\n  module Reporters\n    class ActiveSupportReporter\n      def initialize(instrumentation_key)\n        @instrumentation_key = instrumentation_key\n      end\n\n      def perform(params)\n        ActiveSupport::Notifications.instrument @instrumentation_key, params\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/reporters/logger_reporter.rb",
    "content": "module GrapeLogging\n  module Reporters\n    class LoggerReporter\n      def initialize(logger, formatter, log_level)\n        @logger = logger.clone || Logger.new(STDOUT)\n        @log_level = log_level || :info\n        @logger.formatter = formatter || @logger.formatter || GrapeLogging::Formatters::Default.new if @logger.respond_to?(:formatter=)\n      end\n\n      def perform(params)\n        @logger.send(@log_level, params)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/timings.rb",
    "content": "module GrapeLogging\n  module Timings\n    def self.db_runtime=(value)\n      Thread.current[:grape_db_runtime] = value\n    end\n\n    def self.db_runtime\n      Thread.current[:grape_db_runtime] ||= 0\n    end\n\n    def self.reset_db_runtime\n      self.db_runtime = 0\n    end\n\n    def self.append_db_runtime(event)\n      self.db_runtime += event.duration\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/util/parameter_filter.rb",
    "content": "module GrapeLogging\n  module Util\n    if defined?(Rails.application)\n      if Gem::Version.new(Rails.version) < Gem::Version.new('6.0.0')\n        class ParameterFilter < ActionDispatch::Http::ParameterFilter\n          def initialize(_replacement, filter_parameters)\n            super(filter_parameters)\n          end\n        end\n      else\n        require 'active_support/parameter_filter'\n\n        class ParameterFilter < ActiveSupport::ParameterFilter\n          def initialize(_replacement, filter_parameters)\n            super(filter_parameters)\n          end\n        end\n      end\n    else\n      #\n      # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameter_filter.rb\n      # we could depend on Rails specifically, but that would us way to hefty!\n      #\n      class ParameterFilter\n        def initialize(replacement, filters = [])\n          @replacement = replacement\n          @filters = filters\n        end\n\n        def filter(params)\n          compiled_filter.call(params)\n        end\n\n        private\n\n        def compiled_filter\n          @compiled_filter ||= CompiledFilter.compile(@replacement, @filters)\n        end\n\n        class CompiledFilter # :nodoc:\n          def self.compile(replacement, filters)\n            return ->(params) { params.dup } if filters.empty?\n\n            strings = []\n            regexps = []\n            blocks = []\n\n            filters.each do |item|\n              case item\n              when Proc\n                blocks << item\n              when Regexp\n                regexps << item\n              else\n                strings << Regexp.escape(item.to_s)\n              end\n            end\n\n            deep_regexps, regexps = regexps.partition { |r| r.to_s.include?('\\\\.'.freeze) }\n            deep_strings, strings = strings.partition { |s| s.include?('\\\\.'.freeze) }\n\n            regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty?\n            deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty?\n\n            new replacement, regexps, deep_regexps, blocks\n          end\n\n          attr_reader :regexps, :deep_regexps, :blocks\n\n          def initialize(replacement, regexps, deep_regexps, blocks)\n            @replacement = replacement\n            @regexps = regexps\n            @deep_regexps = deep_regexps.any? ? deep_regexps : nil\n            @blocks = blocks\n          end\n\n          def call(original_params, parents = [])\n            filtered_params = {}\n\n            original_params.each do |key, value|\n              parents.push(key) if deep_regexps\n              if regexps.any? { |r| key =~ r }\n                value = @replacement\n              elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r }\n                value = @replacement\n              elsif value.is_a?(Hash)\n                value = call(value, parents)\n              elsif value.is_a?(Array)\n                value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }\n              elsif blocks.any?\n                key = key.dup if key.duplicable?\n                value = value.dup if value.duplicable?\n                blocks.each { |b| b.call(key, value) }\n              end\n              parents.pop if deep_regexps\n\n              filtered_params[key] = value\n            end\n\n            filtered_params\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/grape_logging/version.rb",
    "content": "module GrapeLogging\n  VERSION = '3.0.1'\nend\n"
  },
  {
    "path": "lib/grape_logging.rb",
    "content": "require 'grape'\nrequire 'rack/utils'\nrequire 'zeitwerk'\n\n# load zeitwerk\nZeitwerk::Loader.for_gem.tap do |loader|\n  loader.inflector.inflect 'multi_io' => 'MultiIO'\n  loader.setup\nend\n\nmodule GrapeLogging\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/formatters/rails_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe GrapeLogging::Formatters::Rails do\n  let(:formatter) { described_class.new }\n  let(:severity) { 'INFO' }\n  let(:datetime) { Time.new('2018', '03', '02', '10', '35', '04', '+13:00') }\n\n  let(:exception_data) { ArgumentError.new('Message') }\n  let(:hash_data) do\n    {\n      status: 200,\n      time: {\n        total: 272.4,\n        db: 40.63,\n        view: 231.76999999999998\n      },\n      method: 'GET',\n      path: '/api/endpoint',\n      host: 'localhost'\n    }\n  end\n\n  describe '#call' do\n    context 'string data' do\n      it 'returns a formatted string' do\n        message = formatter.call(severity, datetime, nil, 'value')\n\n        expect(message).to eq \"I [2018-03-02 10:35:04 +1300] INFO -- : value\\n\"\n      end\n    end\n\n    context 'exception data' do\n      it 'returns a string with a backtrace' do\n        exception_data.set_backtrace(caller)\n\n        message = formatter.call(severity, datetime, nil, exception_data)\n        lines = message.split(\"\\n\")\n\n        expect(lines[0]).to eq 'I [2018-03-02 10:35:04 +1300] INFO -- : Message (ArgumentError)'\n        expect(lines[1]).to include '.rb'\n        expect(lines.size).to be > 1\n      end\n    end\n\n    context 'hash data' do\n      it 'returns a formatted string' do\n        message = formatter.call(severity, datetime, nil, hash_data)\n\n        expect(message).to eq \"Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)\\n\"\n      end\n\n      it 'includes params if included (from GrapeLogging::Loggers::FilterParameters)' do\n        hash_data.merge!(\n          params: {\n            'some_param' => {\n              value_1: '123',\n              value_2: '456'\n            }\n          }\n        )\n\n        message = formatter.call(severity, datetime, nil, hash_data)\n        lines = message.split(\"\\n\")\n\n        expected_output =\n          if RUBY_VERSION >= '3.4'\n            '  Parameters: {\"some_param\" => {value_1: \"123\", value_2: \"456\"}}'\n          else\n            '  Parameters: {\"some_param\"=>{:value_1=>\"123\", :value_2=>\"456\"}}'\n          end\n        expect(lines.first).to eq expected_output\n        expect(lines.last).to eq 'Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)'\n      end\n    end\n\n    context 'unhandled data' do\n      it 'returns the #inspect string representation' do\n        message = formatter.call(severity, datetime, nil, [1, 2, 3])\n\n        expect(message).to eq \"[1, 2, 3]\\n\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/loggers/client_env_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe GrapeLogging::Loggers::ClientEnv do\n  let(:ip) { '10.0.0.1' }\n  let(:user_agent) { 'user agent' }\n  let(:forwarded_for) { 'forwarded for' }\n  let(:remote_addr) { 'remote address' }\n\n  context 'forwarded for' do\n    let(:mock_request) do\n      instance_double(Rack::Request, env: {\n                        'HTTP_X_FORWARDED_FOR' => forwarded_for\n                      })\n    end\n\n    it 'sets the ip key' do\n      expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil)\n    end\n\n    it 'prefers the forwarded_for over the remote_addr' do\n      mock_request.env['REMOTE_ADDR'] = remote_addr\n      expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil)\n    end\n  end\n\n  context 'remote address' do\n    let(:mock_request) do\n      instance_double(Rack::Request, env: {\n                        'REMOTE_ADDR' => remote_addr\n                      })\n    end\n\n    it 'sets the ip key' do\n      expect(subject.parameters(mock_request, nil)).to eq(ip: remote_addr, ua: nil)\n    end\n  end\n\n  context 'user agent' do\n    let(:mock_request) do\n      instance_double(Rack::Request, env: {\n                        'HTTP_USER_AGENT' => user_agent\n                      })\n    end\n\n    it 'sets the ua key' do\n      expect(subject.parameters(mock_request, nil)).to eq(ip: nil, ua: user_agent)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/loggers/filter_parameters_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe GrapeLogging::Loggers::FilterParameters do\n  let(:filtered_parameters) { %w[one four] }\n\n  let(:mock_request) do\n    instance_double(Rack::Request, params: {\n                      'this_one' => 'this one',\n                      'that_one' => 'one',\n                      'two' => 'two',\n                      'three' => 'three',\n                      'four' => 'four',\n                      \"\\xff\" => 'invalid utf8'\n                    })\n  end\n\n  let(:mock_request_with_deep_nesting) do\n    deep_clone = -> { Marshal.load Marshal.dump mock_request.params }\n    instance_double(Rack::Request,\n                    params: deep_clone.call.merge(\n                      'five' => deep_clone.call.merge(\n                        deep_clone.call.merge({ 'six' => { 'seven' => 'seven', 'eight' => 'eight', 'one' => 'another one' } })\n                      )\n                    ))\n  end\n\n  let(:subject) do\n    GrapeLogging::Loggers::FilterParameters.new filtered_parameters, replacement\n  end\n\n  let(:replacement) { nil }\n\n  shared_examples 'filtering' do\n    it 'filters out sensitive parameters' do\n      expect(subject.parameters(mock_request, nil)).to eq(params: {\n                                                            'this_one' => subject.instance_variable_get('@replacement'),\n                                                            'that_one' => subject.instance_variable_get('@replacement'),\n                                                            'two' => 'two',\n                                                            'three' => 'three',\n                                                            'four' => subject.instance_variable_get('@replacement'),\n                                                            \"\\xff\" => 'invalid utf8'\n                                                          })\n    end\n\n    it 'deeply filters out sensitive parameters' do\n      expect(subject.parameters(mock_request_with_deep_nesting, nil)).to eq(params: {\n                                                                              'this_one' => subject.instance_variable_get('@replacement'),\n                                                                              'that_one' => subject.instance_variable_get('@replacement'),\n                                                                              'two' => 'two',\n                                                                              'three' => 'three',\n                                                                              'four' => subject.instance_variable_get('@replacement'),\n                                                                              \"\\xff\" => 'invalid utf8',\n                                                                              'five' => {\n                                                                                'this_one' => subject.instance_variable_get('@replacement'),\n                                                                                'that_one' => subject.instance_variable_get('@replacement'),\n                                                                                'two' => 'two',\n                                                                                'three' => 'three',\n                                                                                'four' => subject.instance_variable_get('@replacement'),\n                                                                                \"\\xff\" => 'invalid utf8',\n                                                                                'six' => {\n                                                                                  'seven' => 'seven',\n                                                                                  'eight' => 'eight',\n                                                                                  'one' => subject.instance_variable_get('@replacement')\n                                                                                }\n                                                                              }\n                                                                            })\n    end\n  end\n\n  context 'with default replacement' do\n    it_behaves_like 'filtering'\n  end\n\n  context 'with custom replacement' do\n    let(:replacement) { 'CUSTOM_REPLACEMENT' }\n    it_behaves_like 'filtering'\n  end\n\n  context 'with symbol keys, which occur during automated testing' do\n    let(:mock_request) { instance_double(Rack::Request, params: { sneaky_symbol: 'hey!' }) }\n\n    it 'converts keys to strings' do\n      expect(subject.parameters(mock_request, nil)).to eq(params: {\n                                                            'sneaky_symbol' => 'hey!'\n                                                          })\n    end\n  end\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/loggers/request_headers_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe GrapeLogging::Loggers::RequestHeaders do\n  let(:mock_request) do\n    instance_double(Rack::Request, env: { HTTP_REFERER: 'http://example.com', HTTP_ACCEPT: 'text/plain' })\n  end\n\n  let(:mock_request_with_unhandled_headers) do\n    instance_double(Rack::Request, env: {\n                      HTTP_REFERER: 'http://example.com',\n                      'PATH_INFO' => '/api/v1/users'\n                    })\n  end\n\n  let(:mock_request_with_long_headers) do\n    instance_double(Rack::Request, env: {\n                      HTTP_REFERER: 'http://example.com',\n                      HTTP_USER_AGENT: 'Mozilla/5.0'\n                    })\n  end\n\n  it 'strips HTTP_ from the parameter' do\n    expect(subject.parameters(mock_request, nil)).to eq(\n      {\n        headers: { 'Referer' => 'http://example.com', 'Accept' => 'text/plain' }\n      }\n    )\n  end\n\n  it 'only handle things which start with HTTP_' do\n    expect(subject.parameters(mock_request_with_unhandled_headers, nil)).to eq(\n      {\n        headers: { 'Referer' => 'http://example.com' }\n      }\n    )\n  end\n\n  it 'substitutes _ with -' do\n    expect(subject.parameters(mock_request_with_long_headers, nil)).to eq(\n      {\n        headers: { 'Referer' => 'http://example.com', 'User-Agent' => 'Mozilla/5.0' }\n      }\n    )\n  end\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/loggers/response_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe GrapeLogging::Loggers::Response do\n  context 'with a parseable JSON body' do\n    let(:response) do\n      instance_double(Rack::Request, body: [{ one: 'two', three: { four: 5 } }])\n    end\n\n    it 'returns an array of parsed JSON objects' do\n      expect(subject.parameters(nil, response)).to eq({ response: [response.body.first] })\n    end\n  end\n\n  context 'with a body that is not parseable JSON' do\n    let(:response) do\n      instance_double(Rack::Request, body: 'this is a body')\n    end\n\n    it 'just returns the body' do\n      expect(subject.parameters(nil, response)).to eq({ response: response.body })\n    end\n  end\nend\n"
  },
  {
    "path": "spec/lib/grape_logging/middleware/request_logger_spec.rb",
    "content": "require 'spec_helper'\nrequire 'rack'\n\ndescribe GrapeLogging::Middleware::RequestLogger do\n  let(:env) { { 'action_dispatch.request_id' => 'request-abc123' } }\n  let(:subject) { request.send(request_method, path, env) }\n  let(:app) { proc { [status, {}, ['response body']] } }\n  let(:stack) { described_class.new app, **options }\n  let(:request) { Rack::MockRequest.new(stack) }\n  let(:options) { { include: [], logger: logger } }\n  let(:logger) { double('logger') }\n  let(:path) { '/' }\n  let(:request_method) { 'get' }\n  let(:status) { 200 }\n\n  it 'logs to the logger' do\n    expect(logger).to receive('info') do |arguments|\n      expect(arguments[:status]).to eq 200\n      expect(arguments[:method]).to eq 'GET'\n      expect(arguments[:params]).to be_empty\n      expect(arguments[:host]).to eq 'example.org'\n      expect(arguments[:request_id]).to eq 'request-abc123'\n      expect(arguments).to have_key :time\n      expect(arguments[:time]).to have_key :total\n      expect(arguments[:time]).to have_key :db\n      expect(arguments[:time]).to have_key :view\n    end\n    subject\n  end\n\n  [301, 404, 500].each do |the_status|\n    context \"when the response status is #{the_status}\" do\n      let(:status) { the_status }\n      it 'should log the correct status code' do\n        expect(logger).to receive('info') do |arguments|\n          expect(arguments[:status]).to eq the_status\n        end\n        subject\n      end\n    end\n  end\n\n  %w[info error debug].each do |level|\n    context \"with level #{level}\" do\n      it 'should log at correct level' do\n        options[:log_level] = level\n        expect(logger).to receive(level)\n        subject\n      end\n    end\n  end\n\n  context 'with a nil response' do\n    let(:app) { proc { [500, {}, nil] } }\n    it 'should log \"fail\" instead of a status' do\n      expect(Rack::MockResponse).to receive(:new) { nil }\n      expect(logger).to receive('info') do |arguments|\n        expect(arguments[:status]).to eq 500\n      end\n      subject\n    end\n  end\n\n  context 'additional_loggers' do\n    before do\n      options[:include] << GrapeLogging::Loggers::RequestHeaders.new\n      options[:include] << GrapeLogging::Loggers::ClientEnv.new\n      options[:include] << GrapeLogging::Loggers::Response.new\n      options[:include] << GrapeLogging::Loggers::FilterParameters.new(['replace_me'])\n    end\n\n    %w[get put post delete options head patch].each do |the_method|\n      let(:request_method) { the_method }\n      context \"with HTTP method[#{the_method}]\" do\n        it 'should include additional information in the log' do\n          expect(logger).to receive('info') do |arguments|\n            expect(arguments).to have_key :headers\n            expect(arguments).to have_key :ip\n            expect(arguments).to have_key :response\n          end\n          subject\n        end\n      end\n    end\n\n    it 'should filter parameters in the log' do\n      expect(logger).to receive('info') do |arguments|\n        expect(arguments[:params]).to eq(\n          'replace_me' => '[FILTERED]',\n          'replace_me_too' => '[FILTERED]',\n          'cant_touch_this' => 'should see'\n        )\n      end\n      parameters = {\n        'replace_me' => 'should not see',\n        'replace_me_too' => 'should not see',\n        'cant_touch_this' => 'should see'\n      }\n      request.post path, params: parameters\n    end\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "$:.unshift '.'\n\nrequire 'lib/grape_logging'\n\nRSpec.configure do |config|\n  # rspec-expectations config goes here. You can use an alternate\n  # assertion/expectation library such as wrong or the stdlib/minitest\n  # assertions if you prefer.\n  config.expect_with :rspec do |expectations|\n    # This option will default to `true` in RSpec 4. It makes the `description`\n    # and `failure_message` of custom matchers include text for helper methods\n    # defined using `chain`, e.g.:\n    #     be_bigger_than(2).and_smaller_than(4).description\n    #     # => \"be bigger than 2 and smaller than 4\"\n    # ...rather than:\n    #     # => \"be bigger than 2\"\n    expectations.include_chain_clauses_in_custom_matcher_descriptions = true\n  end\n\n  # rspec-mocks config goes here. You can use an alternate test double\n  # library (such as bogus or mocha) by changing the `mock_with` option here.\n  config.mock_with :rspec do |mocks|\n    # Prevents you from mocking or stubbing a method that does not exist on\n    # a real object. This is generally recommended, and will default to\n    # `true` in RSpec 4.\n    mocks.verify_partial_doubles = true\n  end\n\n  # This option will default to `:apply_to_host_groups` in RSpec 4 (and will\n  # have no way to turn it off -- the option exists only for backwards\n  # compatibility in RSpec 3). It causes shared context metadata to be\n  # inherited by the metadata hash of host groups and examples, rather than\n  # triggering implicit auto-inclusion in groups with matching metadata.\n  config.shared_context_metadata_behavior = :apply_to_host_groups\n\n  # The settings below are suggested to provide a good initial experience\n  # with RSpec, but feel free to customize to your heart's content.\n  #   # This allows you to limit a spec run to individual examples or groups\n  #   # you care about by tagging them with `:focus` metadata. When nothing\n  #   # is tagged with `:focus`, all examples get run. RSpec also provides\n  #   # aliases for `it`, `describe`, and `context` that include `:focus`\n  #   # metadata: `fit`, `fdescribe` and `fcontext`, respectively.\n  #   config.filter_run_when_matching :focus\n  #\n  #   # Allows RSpec to persist some state between runs in order to support\n  #   # the `--only-failures` and `--next-failure` CLI options. We recommend\n  #   # you configure your source control system to ignore this file.\n  #   config.example_status_persistence_file_path = \"spec/examples.txt\"\n  #\n  #   # Limits the available syntax to the non-monkey patched syntax that is\n  #   # recommended. For more details, see:\n  #   #   - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/\n  #   #   - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/\n  #   #   - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode\n  #   config.disable_monkey_patching!\n  #\n  #   # This setting enables warnings. It's recommended, but in some cases may\n  #   # be too noisy due to issues in dependencies.\n  #   config.warnings = true\n  #\n  #   # Many RSpec users commonly either run the entire suite or an individual\n  #   # file, and it's useful to allow more verbose output when running an\n  #   # individual spec file.\n  #   if config.files_to_run.one?\n  #     # Use the documentation formatter for detailed output,\n  #     # unless a formatter has already been configured\n  #     # (e.g. via a command-line flag).\n  #     config.default_formatter = 'doc'\n  #   end\n  #\n  #   # Print the 10 slowest examples and example groups at the\n  #   # end of the spec run, to help surface which specs are running\n  #   # particularly slow.\n  #   config.profile_examples = 10\n  #\n  #   # Run specs in random order to surface order dependencies. If you find an\n  #   # order dependency and want to debug it, you can fix the order by providing\n  #   # the seed, which is printed after each run.\n  #   #     --seed 1234\n  #   config.order = :random\n  #\n  #   # Seed global randomization in this process using the `--seed` CLI option.\n  #   # Setting this allows you to use `--seed` to deterministically reproduce\n  #   # test failures related to randomization by passing the same `--seed` value\n  #   # as the one that triggered the failure.\n  #   Kernel.srand config.seed\nend\n"
  }
]