Compare commits

..

No commits in common. "master" and "v1.0" have entirely different histories.
master ... v1.0

37 changed files with 73 additions and 1935 deletions

View file

@ -1,36 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["3.0", "3.1", "3.2", "3.3", "3.4", "4.0"]
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Install dependencies
run: bundle install --jobs 4 --retry 3
- name: Run tests
run: bundle exec rake spec
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Run RuboCop
run: bundle exec rake rubocop

2
.gitignore vendored
View file

@ -7,5 +7,3 @@
/pkg/
/spec/reports/
/tmp/
.rspec
.idea/

View file

@ -1,15 +0,0 @@
inherit_from: .rubocop_todo.yml
AllCops:
NewCops: enable
TargetRubyVersion: 3.0
Metrics/BlockLength:
CountAsOne: [array, hash, heredoc, method_call]
Metrics/ClassLength:
CountAsOne: [array, hash, heredoc, method_call]
Metrics/MethodLength:
CountAsOne: [array, hash, heredoc, method_call]
Max: 20

View file

@ -1,160 +0,0 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2025-07-10 10:25:51 UTC using RuboCop version 1.77.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 3
# Configuration parameters: EnforcedStyle, AllowedGems, Include.
# SupportedStyles: Gemfile, gems.rb, gemspec
# Include: **/*.gemspec, **/Gemfile, **/gems.rb
Gemspec/DevelopmentDependencies:
Exclude:
- "grape_logging.gemspec"
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Severity, Include.
# Include: **/*.gemspec
Gemspec/RequireMFA:
Exclude:
- "grape_logging.gemspec"
# Offense count: 1
# Configuration parameters: Severity, Include.
# Include: **/*.gemspec
Gemspec/RequiredRubyVersion:
Exclude:
- "grape_logging.gemspec"
# Offense count: 1
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
Lint/DuplicateBranch:
Exclude:
- "lib/grape_logging/util/parameter_filter.rb"
# Offense count: 1
# Configuration parameters: AllowedParentClasses.
Lint/MissingSuper:
Exclude:
- "lib/grape_logging/loggers/filter_parameters.rb"
# Offense count: 3
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 38
# Offense count: 8
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
# AllowedMethods: refine
Metrics/BlockLength:
Max: 90
# Offense count: 3
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 18
# Offense count: 2
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 19
# Offense count: 2
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
Naming/VariableNumber:
Exclude:
- "spec/lib/grape_logging/formatters/rails_spec.rb"
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: MinBranchesCount.
Style/CaseLikeIf:
Exclude:
- "lib/grape_logging/formatters/default.rb"
- "lib/grape_logging/formatters/logstash.rb"
- "lib/grape_logging/formatters/rails.rb"
# Offense count: 17
# Configuration parameters: AllowedConstants.
Style/Documentation:
Enabled: false
# Offense count: 29
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
Style/FrozenStringLiteralComment:
Enabled: false
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
- "lib/grape_logging/reporters/logger_reporter.rb"
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: literals, strict
Style/MutableConstant:
Exclude:
- "lib/grape_logging/version.rb"
# Offense count: 10
Style/OpenStructUse:
Exclude:
- "spec/lib/grape_logging/loggers/client_env_spec.rb"
- "spec/lib/grape_logging/loggers/filter_parameters_spec.rb"
- "spec/lib/grape_logging/loggers/request_headers_spec.rb"
- "spec/lib/grape_logging/loggers/response_spec.rb"
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- "lib/grape_logging/formatters/rails.rb"
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SlicingWithRange:
Exclude:
- "lib/grape_logging/loggers/request_headers.rb"
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: RequireEnglish, EnforcedStyle.
# SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names
Style/SpecialGlobalVars:
Exclude:
- "spec/spec_helper.rb"
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Mode.
Style/StringConcatenation:
Exclude:
- "lib/grape_logging/formatters/json.rb"
- "lib/grape_logging/formatters/lograge.rb"
- "lib/grape_logging/formatters/logstash.rb"
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments.
# AllowedMethods: define_method
Style/SymbolProc:
Exclude:
- "lib/grape_logging/util/parameter_filter.rb"
# Offense count: 11
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
Layout/LineLength:
Max: 203

View file

@ -1,225 +0,0 @@
# Changelog
## [3.0.1] - Unreleased
### Changed or Fixed or Added
### Changed
- Move dev dependencies to Gemfile
- Use zeitwerk to load gem
[3.0.1]: https://github.com/aserafin/grape_logging/compare/v3.0.0...master
## [3.0.0] - 2025-08-07
### Changed
- [#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).
[3.0.0]: https://github.com/aserafin/grape_logging/compare/v2.1.1...v3.0.0
## [2.1.1] - 2025-07-09
### Fixed
- [#92](https://github.com/aserafin/grape_logging/pull/92) Handle symbol param keys during filtering - [@samsonjs](https://github.com/samsonjs).
[2.1.1]: https://github.com/aserafin/grape_logging/compare/v2.1.0...v2.1.1
## [2.1.0] - 2025-07-09
### Added
- [#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).
[2.1.0]: https://github.com/aserafin/grape_logging/compare/v2.0.0...v2.1.0
## [2.0.0] - 2025-07-09
### Changed
- **BREAKING**: Updated to support Grape 2.1 and Rack 3.1
- Minimum supported Ruby version is now 3.0
- Replaced Travis CI with GitHub Actions for continuous integration
- Updated all README examples to use `insert_before` instead of `use` for proper middleware placement
### Fixed
- Fixed LoggerReporter to clone the logger parameter to prevent shared state issues (#77)
- Fixed view time precision issue by rounding to 2 decimal places
- Fixed invalid byte sequence handling for parameter keys by properly managing string encodings (#54)
- Fixed various typos in code comments and spec descriptions (#87)
- Fixed specs to work with Ruby 3.4's hash inspect format changes
### Documentation
- Clarified middleware placement requirements in README - must be inserted before Grape::Middleware::Error (#74)
- Added gem version badge to README
[2.0.0]: https://github.com/aserafin/grape_logging/compare/v1.8.4...v2.0.0
## [1.8.4] - 2021-10-29
### Fixed
- Rails 6 compatibility improvements
- Various bug fixes and dependency updates
[1.8.4]: https://github.com/aserafin/grape_logging/compare/v1.8.3...v1.8.4
## [1.8.3] - 2020-02-27
### Fixed
- Performance improvements
- Bug fixes for edge cases
[1.8.3]: https://github.com/aserafin/grape_logging/compare/v1.8.2...v1.8.3
## [1.8.2] - 2019-10-08
### Fixed
- Thread safety improvements
- Minor bug fixes
Note: This version was tagged as "v.1.8.2" (with extra dot)
[1.8.2]: https://github.com/aserafin/grape_logging/compare/v1.8.1...v.1.8.2
## [1.8.1] - 2019-08-07
### Fixed
- Bug fixes for parameter filtering
- Improved error handling
[1.8.1]: https://github.com/aserafin/grape_logging/compare/v1.8.0...v1.8.1
## [1.8.0] - 2019-05-30
### Added
- Rails formatter for better Rails integration
- Improved Rails instrumentation support
[1.8.0]: https://github.com/aserafin/grape_logging/compare/v1.7.0...v1.8.0
## [1.7.0] - 2017-11-09
### Added
- Logstash formatter for ELK stack integration
- Enhanced JSON formatting options
[1.7.0]: https://github.com/aserafin/grape_logging/compare/v1.6.0...v1.7.0
## [1.6.0] - 2017-07-20
### Added
- MultiIO support for logging to multiple destinations simultaneously
- Can now log to both file and STDOUT
[1.6.0]: https://github.com/aserafin/grape_logging/compare/v1.5.0...v1.6.0
## [1.5.0] - 2017-06-15
### Added
- Configurable log levels
- Better control over logging verbosity
[1.5.0]: https://github.com/aserafin/grape_logging/compare/v1.4.0...v1.5.0
## [1.4.0] - 2017-01-12
### Added
- FilterParameters logger for sensitive parameter filtering
- Automatic Rails filter_parameters integration when available
[1.4.0]: https://github.com/aserafin/grape_logging/compare/v1.3.0...v1.4.0
## [1.3.0] - 2016-12-08
### Added
- RequestHeaders logger for logging HTTP request headers
- ClientEnv logger for logging client IP and user agent
[1.3.0]: https://github.com/aserafin/grape_logging/compare/v1.2.1...v1.3.0
## [1.2.1] - 2016-04-14
### Added
- JSON formatter for structured logging
- Rails instrumentation support via ActiveSupport::Notifications
### Fixed
- Parameter handling improvements
[1.2.1]: https://github.com/aserafin/grape_logging/compare/v1.2.0...v1.2.1
## [1.2.0] - 2016-01-21
### Added
- Response logger for logging response details
- Improved parameter logging
### Changed
- Better integration with Grape middleware stack
[1.2.0]: https://github.com/aserafin/grape_logging/compare/v1.1.3...v1.2.0
## [1.1.3] - 2015-12-03
### Fixed
- Bug fixes for Grape 0.14 compatibility
- Improved error handling
[1.1.3]: https://github.com/aserafin/grape_logging/compare/v1.1.2...v1.1.3
## [1.1.2] - 2015-11-19
### Fixed
- Performance optimizations
- Minor bug fixes
[1.1.2]: https://github.com/aserafin/grape_logging/compare/v1.1.1...v1.1.2
## [1.1.1] - 2015-11-12
### Fixed
- Critical bug fix for middleware initialization
Note: This version was tagged as "v.1.1.1" (with extra dot)
[1.1.1]: https://github.com/aserafin/grape_logging/compare/v1.1.0...v.1.1.1
## [1.1.0] - 2015-11-09
### Added
- Pluggable logger architecture
- Support for custom loggers via include option
- Base logger class for extending functionality
### Changed
- Refactored middleware for better extensibility
[1.1.0]: https://github.com/aserafin/grape_logging/compare/v1.0.3...v1.1.0
## [1.0.3] - 2015-11-05
### Fixed
- Compatibility fixes for different Grape versions
- Bug fixes
[1.0.3]: https://github.com/aserafin/grape_logging/compare/v1.0.2...v1.0.3
## [1.0.2] - 2015-10-29
### Fixed
- Minor bug fixes and improvements
[1.0.2]: https://github.com/aserafin/grape_logging/compare/v1.0.1...v1.0.2
## [1.0.1] - 2015-10-22
### Fixed
- Initial bug fixes after 1.0 release
[1.0.1]: https://github.com/aserafin/grape_logging/compare/v1.0...v1.0.1
## [1.0] - 2015-10-15
### Added
- Initial release
- Request logging middleware for Grape APIs
- Basic request/response logging
- Configurable formatters
- Time tracking (total, db, view)

View file

@ -1,110 +0,0 @@
# Contributing to grape_logging
This project is work of many contributors. You're encouraged to submit pull requests, propose features and discuss issues.
## Fork the Project
Fork the project on Github and check out your copy.
```
git clone https://github.com/contributor/grape_logging.git
cd grape_logging
git remote add upstream https://github.com/aserafin/grape_logging.git
```
## Create a Topic Branch
Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
```
git checkout master
git pull upstream master
git checkout -b my-feature-branch
```
## Bundle Install and Test
Ensure that you can build the project and run tests.
```
bundle install
bundle exec rake
```
## Write Tests
Try 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.
## Write Code
Implement your feature or bug fix.
Ruby style is enforced with RuboCop. Run `bundle exec rubocop` and fix any style issues highlighted.
Make sure that `bundle exec rake` completes without errors.
## Write Documentation
Document any external behavior in the README.md.
## Update Changelog
Add a line to Changelog.md under *Next Release*. Make it look like every other line, including your name and link to your Github account.
## Commit Changes
Make sure git knows your name and email address:
```
git config --global user.name "Your Name"
git config --global user.email "contributor@example.com"
```
Writing good commit logs is important. A commit log should describe what changed and why.
```
git add ...
git commit
```
## Push
```
git push origin my-feature-branch
```
## Make a Pull Request
Go to https://github.com/contributor/grape_logging and select your feature branch. Click the 'Pull Request' button and fill out the form.
We'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.
## Discuss and Update
You 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!
Some things that will increase the chance that your pull request is accepted:
- Write tests.
- Follow the Ruby style guide.
- Write a good commit message.
If 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.
## Rebase
If you've been working on a change for a while, rebase with upstream/master.
```
git fetch upstream
git rebase upstream/master
git push origin my-feature-branch -f
```
## Be Patient
It'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!
## Thank You
Please do know that we really appreciate and value your time and work. We love you, really.

View file

@ -2,10 +2,3 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in grape_logging.gemspec
gemspec
gem 'rake', '~> 13.3'
gem 'rspec', '~> 3.5'
# This is pinned to an exact version otherwise we can't know which rules
# are in play at any given time in different environments.
gem 'rubocop', '1.77.0'

194
README.md
View file

@ -1,7 +1,6 @@
# grape_logging
[![Gem Version](https://badge.fury.io/rb/grape_logging.svg)](https://badge.fury.io/rb/grape_logging)
[![CI](https://github.com/aserafin/grape_logging/actions/workflows/ci.yml/badge.svg)](https://github.com/aserafin/grape_logging/actions/workflows/ci.yml)
[![Code Climate](https://codeclimate.com/github/aserafin/grape_logging/badges/gpa.svg)](https://codeclimate.com/github/aserafin/grape_logging)
## Installation
@ -11,7 +10,7 @@ Add this line to your application's Gemfile:
And then execute:
$ bundle install
$ bundle
Or install it yourself as:
@ -19,198 +18,49 @@ Or install it yourself as:
## Basic Usage
In 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.
In your api file (somewhere on the top)
```ruby
require 'grape_logging'
logger.formatter = GrapeLogging::Formatters::Default.new
insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, { logger: logger }
```
logger.formatter = GrapeLogging::Formatters::Default.new
use GrapeLogging::Middleware::RequestLogger, { logger: logger }
**ProTip:** If your logger doesn't support setting formatter you can remove this line - it's optional
## Features
### Log Format
There are formatters provided for you, or you can provide your own.
With the default configuration you will get nice log message
#### `GrapeLogging::Formatters::Default`
[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"}}
#### `GrapeLogging::Formatters::Json`
```json
{
"date": "2015-04-16 12:52:12+0200",
"severity": "INFO",
"data": {
"status": 200,
"time": {
"total": 2.06,
"db": 0.36,
"view": 1.70
},
"method": "PATCH",
"path": "/api/endpoint",
"params": {
"value_1": "123",
"value_2": "456"
},
"host": "localhost"
}
}
```
#### `GrapeLogging::Formatters::Lograge`
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"
#### `GrapeLogging::Formatters::Logstash`
```json
{
"@timestamp": "2015-04-16 12:52:12+0200",
"severity": "INFO",
"status": 200,
"time": {
"total": 2.06,
"db": 0.36,
"view": 1.70
},
"method": "PATCH",
"path": "/api/endpoint",
"params": {
"value_1": "123",
"value_2": "456"
},
"host": "localhost"
}
```
#### `GrapeLogging::Formatters::Rails`
Rails will print the "Started..." line:
Started GET "/api/endpoint" for ::1 at 2015-04-16 12:52:12 +0200
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1
...
The `Rails` formatter adds the last line of the request, like a standard Rails request:
Completed 200 OK in 349ms (Views: 250.1ms | DB: 98.63ms)
#### Custom
You can provide your own class that implements the `call` method returning a `String`:
```ruby
def call(severity, datetime, _, data)
...
end
```
You can change the formatter like so
```ruby
class MyAPI < Grape::API
insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, logger: logger, formatter: MyFormatter.new
end
```
[2015-04-16 12:52:12 +0200] INFO -- 200 -- total=2.06 db=0.36 -- PATCH /your_app/endpoint params={"some_param"=>{"value_1"=>"123", "value_2"=>"456"}}
If you prefer some other format I strongly encourage you to do pull request with new formatter class ;)
### Customising What Is Logged
You can include logging of other parts of the request / response cycle by including subclasses of `GrapeLogging::Loggers::Base`
```ruby
class MyAPI < Grape::API
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
logger: logger,
include: [ GrapeLogging::Loggers::Response.new,
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
GrapeLogging::Loggers::RequestHeaders.new ]
end
```
#### FilterParameters
The `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.
#### ClientEnv
The `ClientEnv` logger will add `ip` and user agent `ua` in your log.
#### RequestHeaders
The `RequestHeaders` logger will add `request headers` in your log.
### Logging to file and STDOUT
You can log to file and STDOUT at the same time, you just need to assign new logger
```ruby
log_file = File.open('path/to/your/logfile.log', 'a')
log_file.sync = true
logger Logger.new GrapeLogging::MultiIO.new(STDOUT, log_file)
```
You can to file and STDOUT at the same time, you just need to assign new logger
### Set the log level
You can control the level used to log. The default is `info`.
```ruby
class MyAPI < Grape::API
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
logger: logger,
log_level: 'debug'
end
```
### Logging via Rails instrumentation
You 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.
First, config ```grape_logging```, like that:
```ruby
class MyAPI < Grape::API
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
instrumentation_key: 'grape_key',
include: [ GrapeLogging::Loggers::Response.new,
GrapeLogging::Loggers::FilterParameters.new ]
end
```
and then add an initializer in your Rails project:
```ruby
# config/initializers/instrumentation.rb
# Subscribe to grape request and log with Rails.logger
ActiveSupport::Notifications.subscribe('grape_key') do |name, starts, ends, notification_id, payload|
Rails.logger.info payload
end
```
The idea come from here: https://gist.github.com/teamon/e8ae16ffb0cb447e5b49
logger Logger.new GrapeLogging::MultiIO.new(STDOUT, File.open('path/to/your/logfile.log'), 'a'))
### Logging exceptions
If you want to log exceptions you can do it like this
```ruby
class MyAPI < Grape::API
rescue_from :all do |e|
MyAPI.logger.error e
#do here whatever you originally planned to do :)
end
end
```
class MyAPI < Grape::API
rescue_from :all do |e|
MyAPI.logger.error e
#do here whatever you originally planned to do :)
end
end
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
For maintainers releasing a new version, please see [RELEASING.md](RELEASING.md).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
1. Fork it ( https://github.com/aserafin/grape_logging/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

View file

@ -1,113 +0,0 @@
# Releasing grape_logging
There're no particular rules about when to release grape_logging. Release bug fixes frequently, features not so frequently and breaking API changes rarely.
### Pre-flight Checks
Run tests, check that all tests succeed locally.
```
bundle install
rake
```
Check that the last build succeeded in [GitHub Actions](https://github.com/aserafin/grape_logging/actions) for all supported platforms.
### Update Changelog
Change "Unreleased" in [CHANGELOG.md](https://github.com/aserafin/grape_logging/blob/master/CHANGELOG.md) to the new version and date:
```
## [1.8.5] - 2024-06-28
### Changed
- Description of changes
### Fixed
- Description of fixes
### Added
- Description of additions
[1.8.5]: https://github.com/aserafin/grape_logging/compare/v1.8.4...v1.8.5
```
Remove the line with "Your contribution here.", since there will be no more contributions to this release.
Only include the sections (Changed, Fixed, Added, etc.) that have actual changes.
Commit your changes.
```shell
git add CHANGELOG.md lib/grape_logging/version.rb
git commit -m "Preparing for release, 1.8.5."
git push
```
### Release on RubyGems and GitHub
#### Option 1: Automated (Recommended)
Use the combined task that releases the gem and creates a GitHub release:
```shell
rake github_release
```
This will:
1. Build and push the gem to RubyGems
2. Create and push the git tag
3. Create a GitHub release with auto-generated changelog
#### Option 2: Manual
First, release the gem:
```shell
rake release
```
Output will look something like:
```
grape_logging 1.8.5 built to pkg/grape_logging-1.8.5.gem.
Tagged v1.8.5.
Pushed git commits and tags.
Pushed grape_logging 1.8.5 to rubygems.org.
```
Then create the GitHub release on the web or using `gh`:
```
gh release create v1.8.5 --generate-notes --verify-tag
```
This uses GitHub's automatic changelog generation feature to create release notes from merged pull requests and commits since the last release.
### Prepare for the Next Version
Modify `lib/grape_logging/version.rb`, increment the version number (eg. change `1.8.5` to `1.8.6`).
```ruby
module GrapeLogging
VERSION = '1.8.6'.freeze
end
```
Add the next release to [CHANGELOG.md](https://github.com/aserafin/grape_logging/blob/master/CHANGELOG.md).
```
## [1.8.6] - Unreleased
### Changed or Fixed or Added
- Your contribution here.
[1.8.6]: https://github.com/aserafin/grape_logging/compare/v1.8.5...master
```
Commit your changes.
```
git add CHANGELOG.md lib/grape_logging/version.rb
git commit -m "Bump version to 1.8.6."
git push
```

View file

@ -1,43 +1,2 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.rspec_opts = ['-fd -c']
spec.pattern = FileList['spec/**/*_spec.rb']
end
RuboCop::RakeTask.new(:rubocop) do |t|
t.patterns = ['lib/**/*.rb', 'spec/**/*.rb', 'Rakefile', 'Gemfile', 'grape_logging.gemspec']
end
task default: %i[spec rubocop]
desc 'Release gem and create GitHub release'
task github_release: :release do
require 'grape_logging/version'
version = "v#{GrapeLogging::VERSION}"
# Check if gh CLI is available
unless system('which gh > /dev/null 2>&1')
puts "\n⚠️ GitHub CLI (gh) not found"
puts 'To create a GitHub release with auto-generated changelog, install gh:'
puts ' brew install gh # macOS with Homebrew'
puts ' # or visit: https://github.com/cli/cli#installation'
puts "\nYou can manually create the release with:"
puts " gh release create v#{GrapeLogging::VERSION} --generate-notes"
next
end
# Create GitHub release
puts "\nCreating GitHub release #{version}..."
if system('gh', 'release', 'create', version, '--generate-notes', '--verify-tag')
puts "✅ GitHub release #{version} created successfully"
else
puts '❌ Failed to create GitHub release'
puts 'You can manually create it with:'
puts " gh release create '#{version}' --generate-notes --verify-tag"
end
end

0
bin/console Executable file → Normal file
View file

View file

@ -1,26 +1,30 @@
lib = File.expand_path('lib', __dir__)
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'grape_logging/version'
Gem::Specification.new do |spec|
spec.name = 'grape_logging'
spec.version = GrapeLogging::VERSION
spec.authors = ['aserafin', 'Sami Samhuri']
spec.email = ['adrian@softmad.pl', 'sami@samhuri.net']
spec.authors = ['aserafin']
spec.email = ['adrian@softmad.pl']
spec.summary = 'Out of the box request logging for Grape!'
spec.description = 'This gem provides simple request logging for Grape with just few lines ' \
'of code you have to put in your project! In return you will get response ' \
'codes, paths, parameters and more!'
if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com' to prevent pushes to rubygems.org, or delete to allow pushes to any server."
end
spec.summary = %q{Out of the box request logging for Grape!}
spec.description = %q{This gem provides simple request logging for Grape with just 2 lines of code you have to put in your project! In return you will get response codes, parameters, total response duration and time spent in db (if you are using ActiveRecord.)}
spec.homepage = 'http://github.com/aserafin/grape_logging'
spec.license = 'MIT'
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = 'exe'
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
spec.add_dependency 'grape', '>= 2.4.0'
spec.add_dependency 'rack'
spec.add_dependency 'zeitwerk'
end
spec.add_dependency 'grape'
spec.add_development_dependency 'bundler', '~> 1.8'
spec.add_development_dependency 'rake', '~> 10.0'
end

View file

@ -1,12 +1,4 @@
require 'grape'
require 'rack/utils'
require 'zeitwerk'
# load zeitwerk
Zeitwerk::Loader.for_gem.tap do |loader|
loader.inflector.inflect 'multi_io' => 'MultiIO'
loader.setup
end
module GrapeLogging
end
require 'grape_logging/multi_io'
require 'grape_logging/version'
require 'grape_logging/formatters/default'
require 'grape_logging/middleware/request_logger'

View file

@ -11,14 +11,13 @@ module GrapeLogging
elsif data.is_a?(Exception)
format_exception(data)
elsif data.is_a?(Hash)
"#{data.delete(:status)} -- #{format_hash(data.delete(:time))} -- #{data.delete(:method)} #{data.delete(:path)} #{format_hash(data)}"
"#{data.delete(:status)} -- total=#{data.delete(:total)} db=#{data.delete(:db)} -- #{data.delete(:method)} #{data.delete(:path)} #{format_hash(data)}"
else
data.inspect
end
end
private
def format_hash(hash)
hash.keys.sort.map { |key| "#{key}=#{hash[key]}" }.join(' ')
end
@ -29,4 +28,4 @@ module GrapeLogging
end
end
end
end
end

View file

@ -1,33 +0,0 @@
module GrapeLogging
module Formatters
class Json
def call(severity, datetime, _, data)
{
date: datetime,
severity: severity,
data: format(data)
}.to_json + "\n"
end
private
def format(data)
if data.is_a?(String) || data.is_a?(Hash)
data
elsif data.is_a?(Exception)
format_exception(data)
else
data.inspect
end
end
def format_exception(exception)
{
exception: {
message: exception.message
}
}
end
end
end
end

View file

@ -1,17 +0,0 @@
module GrapeLogging
module Formatters
class Lograge
def call(severity, datetime, _, data)
time = data.delete :time
attributes = {
severity: severity,
duration: time[:total],
db: time[:db],
view: time[:view],
datetime: datetime.iso8601
}.merge(data)
::Lograge.formatter.call(attributes) + "\n"
end
end
end
end

View file

@ -1,35 +0,0 @@
module GrapeLogging
module Formatters
class Logstash
def call(severity, datetime, _, data)
{
'@timestamp': datetime.iso8601,
'@version': '1',
severity: severity
}.merge!(format(data)).to_json + "\n"
end
private
def format(data)
if data.is_a?(Hash)
data
elsif data.is_a?(String)
{ message: data }
elsif data.is_a?(Exception)
format_exception(data)
else
{ message: data.inspect }
end
end
def format_exception(exception)
{
exception: {
message: exception.message
}
}
end
end
end
end

View file

@ -1,55 +0,0 @@
module GrapeLogging
module Formatters
class Rails
def call(severity, datetime, _, data)
if data.is_a?(String)
"#{severity[0..0]} [#{datetime}] #{severity} -- : #{data}\n"
elsif data.is_a?(Exception)
"#{severity[0..0]} [#{datetime}] #{severity} -- : #{format_exception(data)}\n"
elsif data.is_a?(Hash)
format_hash(data)
else
"#{data.inspect}\n"
end
end
private
def format_exception(exception)
backtrace_array = (exception.backtrace || []).map { |line| "\t#{line}" }
[
"#{exception.message} (#{exception.class})",
backtrace_array.join("\n")
].reject { |line| line == '' }.join("\n")
end
def format_hash(hash)
# Create Rails' single summary line at the end of every request, formatted like:
# Completed 200 OK in 958ms (Views: 951.1ms | ActiveRecord: 3.8ms)
# See: actionpack/lib/action_controller/log_subscriber.rb
message = ''
additions = []
status = hash.delete(:status)
params = hash.delete(:params)
total_time = hash[:time] && hash[:time][:total] && hash[:time][:total].round(2)
view_time = hash[:time] && hash[:time][:view] && hash[:time][:view].round(2)
db_time = hash[:time] && hash[:time][:db] && hash[:time][:db].round(2)
additions << "Views: #{view_time}ms" if view_time
additions << "DB: #{db_time}ms" if db_time
message << " Parameters: #{params.inspect}\n" if params
message << "Completed #{status} #{::Rack::Utils::HTTP_STATUS_CODES[status]} in #{total_time}ms"
message << " (#{additions.join(' | '.freeze)})" unless additions.empty?
message << "\n"
message << "\n" if defined?(::Rails.env) && ::Rails.env.development?
message
end
end
end
end

View file

@ -1,9 +0,0 @@
module GrapeLogging
module Loggers
class Base
def parameters(_request, _response)
{}
end
end
end
end

View file

@ -1,9 +0,0 @@
module GrapeLogging
module Loggers
class ClientEnv < GrapeLogging::Loggers::Base
def parameters(request, _)
{ ip: request.env['HTTP_X_FORWARDED_FOR'] || request.env['REMOTE_ADDR'], ua: request.env['HTTP_USER_AGENT'] }
end
end
end
end

View file

@ -1,54 +0,0 @@
module GrapeLogging
module Loggers
class FilterParameters < GrapeLogging::Loggers::Base
AD_PARAMS = 'action_dispatch.request.parameters'.freeze
def initialize(filter_parameters = nil, replacement = nil, exceptions = %w[controller action format])
@filter_parameters = filter_parameters || (defined?(::Rails.application) ? ::Rails.application.config.filter_parameters : [])
@replacement = replacement || '[FILTERED]'
@exceptions = exceptions
end
def parameters(request, _)
{ params: safe_parameters(request) }
end
private
def parameter_filter
@parameter_filter ||= GrapeLogging::Util::ParameterFilter.new(@replacement, @filter_parameters)
end
def safe_parameters(request)
# Now this logger can work also over Rails requests
if request.params.empty?
clean_parameters(request.env[AD_PARAMS] || {})
else
clean_parameters(request.params)
end
end
def clean_parameters(parameters)
original_encoding_map = build_encoding_map(parameters)
params = transform_key_encoding(parameters, Hash.new { |h, _| [Encoding::ASCII_8BIT, h] })
cleaned_params = parameter_filter.filter(params).except(*@exceptions)
transform_key_encoding(cleaned_params, original_encoding_map)
end
def build_encoding_map(parameters)
parameters.each_with_object({}) do |(k, v), h|
key_str = k.to_s
h[key_str.dup.force_encoding(Encoding::ASCII_8BIT)] = [key_str.encoding, v.is_a?(Hash) ? build_encoding_map(v) : nil]
end
end
def transform_key_encoding(parameters, encoding_map)
parameters.each_with_object({}) do |(k, v), h|
key_str = k.to_s
encoding, children_encoding_map = encoding_map[key_str]
h[key_str.dup.force_encoding(encoding)] = v.is_a?(Hash) ? transform_key_encoding(v, children_encoding_map) : v
end
end
end
end
end

View file

@ -1,20 +0,0 @@
module GrapeLogging
module Loggers
class RequestHeaders < GrapeLogging::Loggers::Base
HTTP_PREFIX = 'HTTP_'.freeze
def parameters(request, _)
headers = {}
request.env.each_pair do |k, v|
next unless k.to_s.start_with? HTTP_PREFIX
k = k[5..-1].split('_').each(&:capitalize!).join('-')
headers[k] = v
end
{ headers: headers }
end
end
end
end

View file

@ -1,28 +0,0 @@
module GrapeLogging
module Loggers
class Response < GrapeLogging::Loggers::Base
def parameters(_, response)
response ? { response: serialized_response_body(response) } : {}
end
private
# In some cases, response.body is not parseable by JSON.
# For example, if you POST on a PUT endpoint, response.body is egal to """".
# It's strange, but it's the Grape behavior...
def serialized_response_body(response)
if response.respond_to?(:body)
# Rack responses
begin
response.body.map { |body| JSON.parse(body.to_s) }
rescue StandardError # No reason to have "=> e" here when we don't use it..
response.body
end
else
# Error & Exception responses
response
end
end
end
end
end

View file

@ -1,153 +1,46 @@
require 'grape/middleware/base'
module GrapeLogging
module Middleware
class RequestLogger < Grape::Middleware::Base
if defined?(ActiveRecord)
def before
env[:db_duration] = 0
ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
GrapeLogging::Timings.append_db_runtime(event)
end
env[:db_duration] += event.duration
end if defined?(ActiveRecord)
end
# Persist response status & response (body)
# to use int in parameters
attr_accessor :response_status, :response_body
def initialize(app, **options)
super
@included_loggers = @options[:include] || []
@reporter =
if options[:instrumentation_key]
Reporters::ActiveSupportReporter.new(@options[:instrumentation_key])
else
Reporters::LoggerReporter.new(@options[:logger], @options[:formatter], @options[:log_level])
end
end
def before
reset_db_runtime
start_time
invoke_included_loggers(:before)
end
def after(status, response)
stop_time
# Response status
@response_status = status
@response_body = response
# Perform reporters
@reporter.perform(collect_parameters)
# Invoke loggers
invoke_included_loggers(:after)
nil
end
# Call stack and parse responses & status.
#
# @note Exceptions are logged as 500 status & re-raised.
def call!(env)
@env = env
original_response = nil
# Before hook
before
duration = Benchmark.realtime { original_response = super(env) }
logger.info parameters(original_response, duration)
# Catch error
error = catch(:error) do
@app_response = @app.call(@env)
nil
rescue StandardError => e
# Log as 500 + message
after(e.respond_to?(:status) ? e.status : 500, e.message)
# Re-raise exception
raise e
end
# Get status & response from app_response
# when no error occurs.
if error
# Call with error & response
after(error[:status], error[:message])
# Throw again
throw(:error, error)
else
status, _, resp = *@app_response
# Call after hook properly
after(status, resp)
end
# Otherwise return original response
@app_response
original_response
end
protected
def parameters
{
status: response_status,
time: {
total: total_runtime,
db: db_runtime,
view: view_runtime
},
method: request.request_method,
path: request.path,
params: request.params,
host: request.host,
request_id: env['action_dispatch.request_id']
}
end
def parameters(response, duration)
{
path: request.path,
params: request.params,
method: request.request_method,
total: (duration * 1000).round(2),
db: request.env[:db_duration].round(2),
status: response.first
}
end
private
def logger
@logger ||= @options[:logger] || Logger.new(STDOUT)
end
def request
@request ||= ::Rack::Request.new(@env)
end
def total_runtime
((stop_time - start_time) * 1000).round(2)
end
def view_runtime
(total_runtime - db_runtime).round(2)
end
def db_runtime
GrapeLogging::Timings.db_runtime.round(2)
end
def reset_db_runtime
GrapeLogging::Timings.reset_db_runtime
end
def start_time
@start_time ||= Time.now
end
def stop_time
@stop_time ||= Time.now
end
def collect_parameters
parameters.tap do |params|
@included_loggers.each do |logger|
params.merge! logger.parameters(request, response_body) do |_, oldval, newval|
oldval.respond_to?(:merge) ? oldval.merge(newval) : newval
end
end
end
end
def invoke_included_loggers(method_name)
@included_loggers.each do |logger|
logger.send(method_name) if logger.respond_to?(method_name)
end
@request ||= ::Rack::Request.new(env)
end
end
end
end
end

View file

@ -5,11 +5,11 @@ module GrapeLogging
end
def write(*args)
@targets.each { |t| t.write(*args) }
@targets.each {|t| t.write(*args)}
end
def close
@targets.each(&:close)
end
end
end
end

View file

@ -1,13 +0,0 @@
module GrapeLogging
module Reporters
class ActiveSupportReporter
def initialize(instrumentation_key)
@instrumentation_key = instrumentation_key
end
def perform(params)
ActiveSupport::Notifications.instrument @instrumentation_key, params
end
end
end
end

View file

@ -1,15 +0,0 @@
module GrapeLogging
module Reporters
class LoggerReporter
def initialize(logger, formatter, log_level)
@logger = logger.clone || Logger.new(STDOUT)
@log_level = log_level || :info
@logger.formatter = formatter || @logger.formatter || GrapeLogging::Formatters::Default.new if @logger.respond_to?(:formatter=)
end
def perform(params)
@logger.send(@log_level, params)
end
end
end
end

View file

@ -1,19 +0,0 @@
module GrapeLogging
module Timings
def self.db_runtime=(value)
Thread.current[:grape_db_runtime] = value
end
def self.db_runtime
Thread.current[:grape_db_runtime] ||= 0
end
def self.reset_db_runtime
self.db_runtime = 0
end
def self.append_db_runtime(event)
self.db_runtime += event.duration
end
end
end

View file

@ -1,106 +0,0 @@
module GrapeLogging
module Util
if defined?(Rails.application)
if Gem::Version.new(Rails.version) < Gem::Version.new('6.0.0')
class ParameterFilter < ActionDispatch::Http::ParameterFilter
def initialize(_replacement, filter_parameters)
super(filter_parameters)
end
end
else
require 'active_support/parameter_filter'
class ParameterFilter < ActiveSupport::ParameterFilter
def initialize(_replacement, filter_parameters)
super(filter_parameters)
end
end
end
else
#
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameter_filter.rb
# we could depend on Rails specifically, but that would us way to hefty!
#
class ParameterFilter
def initialize(replacement, filters = [])
@replacement = replacement
@filters = filters
end
def filter(params)
compiled_filter.call(params)
end
private
def compiled_filter
@compiled_filter ||= CompiledFilter.compile(@replacement, @filters)
end
class CompiledFilter # :nodoc:
def self.compile(replacement, filters)
return ->(params) { params.dup } if filters.empty?
strings = []
regexps = []
blocks = []
filters.each do |item|
case item
when Proc
blocks << item
when Regexp
regexps << item
else
strings << Regexp.escape(item.to_s)
end
end
deep_regexps, regexps = regexps.partition { |r| r.to_s.include?('\\.'.freeze) }
deep_strings, strings = strings.partition { |s| s.include?('\\.'.freeze) }
regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty?
deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty?
new replacement, regexps, deep_regexps, blocks
end
attr_reader :regexps, :deep_regexps, :blocks
def initialize(replacement, regexps, deep_regexps, blocks)
@replacement = replacement
@regexps = regexps
@deep_regexps = deep_regexps.any? ? deep_regexps : nil
@blocks = blocks
end
def call(original_params, parents = [])
filtered_params = {}
original_params.each do |key, value|
parents.push(key) if deep_regexps
if regexps.any? { |r| key =~ r }
value = @replacement
elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r }
value = @replacement
elsif value.is_a?(Hash)
value = call(value, parents)
elsif value.is_a?(Array)
value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }
elsif blocks.any?
key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
blocks.each { |b| b.call(key, value) }
end
parents.pop if deep_regexps
filtered_params[key] = value
end
filtered_params
end
end
end
end
end
end

View file

@ -1,3 +1,3 @@
module GrapeLogging
VERSION = '3.0.1'
VERSION = '1.0.0'
end

View file

@ -1,84 +0,0 @@
require 'spec_helper'
describe GrapeLogging::Formatters::Rails do
let(:formatter) { described_class.new }
let(:severity) { 'INFO' }
let(:datetime) { Time.new('2018', '03', '02', '10', '35', '04', '+13:00') }
let(:exception_data) { ArgumentError.new('Message') }
let(:hash_data) do
{
status: 200,
time: {
total: 272.4,
db: 40.63,
view: 231.76999999999998
},
method: 'GET',
path: '/api/endpoint',
host: 'localhost'
}
end
describe '#call' do
context 'string data' do
it 'returns a formatted string' do
message = formatter.call(severity, datetime, nil, 'value')
expect(message).to eq "I [2018-03-02 10:35:04 +1300] INFO -- : value\n"
end
end
context 'exception data' do
it 'returns a string with a backtrace' do
exception_data.set_backtrace(caller)
message = formatter.call(severity, datetime, nil, exception_data)
lines = message.split("\n")
expect(lines[0]).to eq 'I [2018-03-02 10:35:04 +1300] INFO -- : Message (ArgumentError)'
expect(lines[1]).to include '.rb'
expect(lines.size).to be > 1
end
end
context 'hash data' do
it 'returns a formatted string' do
message = formatter.call(severity, datetime, nil, hash_data)
expect(message).to eq "Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)\n"
end
it 'includes params if included (from GrapeLogging::Loggers::FilterParameters)' do
hash_data.merge!(
params: {
'some_param' => {
value_1: '123',
value_2: '456'
}
}
)
message = formatter.call(severity, datetime, nil, hash_data)
lines = message.split("\n")
expected_output =
if RUBY_VERSION >= '3.4'
' Parameters: {"some_param" => {value_1: "123", value_2: "456"}}'
else
' Parameters: {"some_param"=>{:value_1=>"123", :value_2=>"456"}}'
end
expect(lines.first).to eq expected_output
expect(lines.last).to eq 'Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)'
end
end
context 'unhandled data' do
it 'returns the #inspect string representation' do
message = formatter.call(severity, datetime, nil, [1, 2, 3])
expect(message).to eq "[1, 2, 3]\n"
end
end
end
end

View file

@ -1,49 +0,0 @@
require 'spec_helper'
describe GrapeLogging::Loggers::ClientEnv do
let(:ip) { '10.0.0.1' }
let(:user_agent) { 'user agent' }
let(:forwarded_for) { 'forwarded for' }
let(:remote_addr) { 'remote address' }
context 'forwarded for' do
let(:mock_request) do
instance_double(Rack::Request, env: {
'HTTP_X_FORWARDED_FOR' => forwarded_for
})
end
it 'sets the ip key' do
expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil)
end
it 'prefers the forwarded_for over the remote_addr' do
mock_request.env['REMOTE_ADDR'] = remote_addr
expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil)
end
end
context 'remote address' do
let(:mock_request) do
instance_double(Rack::Request, env: {
'REMOTE_ADDR' => remote_addr
})
end
it 'sets the ip key' do
expect(subject.parameters(mock_request, nil)).to eq(ip: remote_addr, ua: nil)
end
end
context 'user agent' do
let(:mock_request) do
instance_double(Rack::Request, env: {
'HTTP_USER_AGENT' => user_agent
})
end
it 'sets the ua key' do
expect(subject.parameters(mock_request, nil)).to eq(ip: nil, ua: user_agent)
end
end
end

View file

@ -1,88 +0,0 @@
require 'spec_helper'
describe GrapeLogging::Loggers::FilterParameters do
let(:filtered_parameters) { %w[one four] }
let(:mock_request) do
instance_double(Rack::Request, params: {
'this_one' => 'this one',
'that_one' => 'one',
'two' => 'two',
'three' => 'three',
'four' => 'four',
"\xff" => 'invalid utf8'
})
end
let(:mock_request_with_deep_nesting) do
deep_clone = -> { Marshal.load Marshal.dump mock_request.params }
instance_double(Rack::Request,
params: deep_clone.call.merge(
'five' => deep_clone.call.merge(
deep_clone.call.merge({ 'six' => { 'seven' => 'seven', 'eight' => 'eight', 'one' => 'another one' } })
)
))
end
let(:subject) do
GrapeLogging::Loggers::FilterParameters.new filtered_parameters, replacement
end
let(:replacement) { nil }
shared_examples 'filtering' do
it 'filters out sensitive parameters' do
expect(subject.parameters(mock_request, nil)).to eq(params: {
'this_one' => subject.instance_variable_get('@replacement'),
'that_one' => subject.instance_variable_get('@replacement'),
'two' => 'two',
'three' => 'three',
'four' => subject.instance_variable_get('@replacement'),
"\xff" => 'invalid utf8'
})
end
it 'deeply filters out sensitive parameters' do
expect(subject.parameters(mock_request_with_deep_nesting, nil)).to eq(params: {
'this_one' => subject.instance_variable_get('@replacement'),
'that_one' => subject.instance_variable_get('@replacement'),
'two' => 'two',
'three' => 'three',
'four' => subject.instance_variable_get('@replacement'),
"\xff" => 'invalid utf8',
'five' => {
'this_one' => subject.instance_variable_get('@replacement'),
'that_one' => subject.instance_variable_get('@replacement'),
'two' => 'two',
'three' => 'three',
'four' => subject.instance_variable_get('@replacement'),
"\xff" => 'invalid utf8',
'six' => {
'seven' => 'seven',
'eight' => 'eight',
'one' => subject.instance_variable_get('@replacement')
}
}
})
end
end
context 'with default replacement' do
it_behaves_like 'filtering'
end
context 'with custom replacement' do
let(:replacement) { 'CUSTOM_REPLACEMENT' }
it_behaves_like 'filtering'
end
context 'with symbol keys, which occur during automated testing' do
let(:mock_request) { instance_double(Rack::Request, params: { sneaky_symbol: 'hey!' }) }
it 'converts keys to strings' do
expect(subject.parameters(mock_request, nil)).to eq(params: {
'sneaky_symbol' => 'hey!'
})
end
end
end

View file

@ -1,45 +0,0 @@
require 'spec_helper'
describe GrapeLogging::Loggers::RequestHeaders do
let(:mock_request) do
instance_double(Rack::Request, env: { HTTP_REFERER: 'http://example.com', HTTP_ACCEPT: 'text/plain' })
end
let(:mock_request_with_unhandled_headers) do
instance_double(Rack::Request, env: {
HTTP_REFERER: 'http://example.com',
'PATH_INFO' => '/api/v1/users'
})
end
let(:mock_request_with_long_headers) do
instance_double(Rack::Request, env: {
HTTP_REFERER: 'http://example.com',
HTTP_USER_AGENT: 'Mozilla/5.0'
})
end
it 'strips HTTP_ from the parameter' do
expect(subject.parameters(mock_request, nil)).to eq(
{
headers: { 'Referer' => 'http://example.com', 'Accept' => 'text/plain' }
}
)
end
it 'only handle things which start with HTTP_' do
expect(subject.parameters(mock_request_with_unhandled_headers, nil)).to eq(
{
headers: { 'Referer' => 'http://example.com' }
}
)
end
it 'substitutes _ with -' do
expect(subject.parameters(mock_request_with_long_headers, nil)).to eq(
{
headers: { 'Referer' => 'http://example.com', 'User-Agent' => 'Mozilla/5.0' }
}
)
end
end

View file

@ -1,23 +0,0 @@
require 'spec_helper'
describe GrapeLogging::Loggers::Response do
context 'with a parseable JSON body' do
let(:response) do
instance_double(Rack::Request, body: [{ one: 'two', three: { four: 5 } }])
end
it 'returns an array of parsed JSON objects' do
expect(subject.parameters(nil, response)).to eq({ response: [response.body.first] })
end
end
context 'with a body that is not parseable JSON' do
let(:response) do
instance_double(Rack::Request, body: 'this is a body')
end
it 'just returns the body' do
expect(subject.parameters(nil, response)).to eq({ response: response.body })
end
end
end

View file

@ -1,102 +0,0 @@
require 'spec_helper'
require 'rack'
describe GrapeLogging::Middleware::RequestLogger do
let(:env) { { 'action_dispatch.request_id' => 'request-abc123' } }
let(:subject) { request.send(request_method, path, env) }
let(:app) { proc { [status, {}, ['response body']] } }
let(:stack) { described_class.new app, **options }
let(:request) { Rack::MockRequest.new(stack) }
let(:options) { { include: [], logger: logger } }
let(:logger) { double('logger') }
let(:path) { '/' }
let(:request_method) { 'get' }
let(:status) { 200 }
it 'logs to the logger' do
expect(logger).to receive('info') do |arguments|
expect(arguments[:status]).to eq 200
expect(arguments[:method]).to eq 'GET'
expect(arguments[:params]).to be_empty
expect(arguments[:host]).to eq 'example.org'
expect(arguments[:request_id]).to eq 'request-abc123'
expect(arguments).to have_key :time
expect(arguments[:time]).to have_key :total
expect(arguments[:time]).to have_key :db
expect(arguments[:time]).to have_key :view
end
subject
end
[301, 404, 500].each do |the_status|
context "when the response status is #{the_status}" do
let(:status) { the_status }
it 'should log the correct status code' do
expect(logger).to receive('info') do |arguments|
expect(arguments[:status]).to eq the_status
end
subject
end
end
end
%w[info error debug].each do |level|
context "with level #{level}" do
it 'should log at correct level' do
options[:log_level] = level
expect(logger).to receive(level)
subject
end
end
end
context 'with a nil response' do
let(:app) { proc { [500, {}, nil] } }
it 'should log "fail" instead of a status' do
expect(Rack::MockResponse).to receive(:new) { nil }
expect(logger).to receive('info') do |arguments|
expect(arguments[:status]).to eq 500
end
subject
end
end
context 'additional_loggers' do
before do
options[:include] << GrapeLogging::Loggers::RequestHeaders.new
options[:include] << GrapeLogging::Loggers::ClientEnv.new
options[:include] << GrapeLogging::Loggers::Response.new
options[:include] << GrapeLogging::Loggers::FilterParameters.new(['replace_me'])
end
%w[get put post delete options head patch].each do |the_method|
let(:request_method) { the_method }
context "with HTTP method[#{the_method}]" do
it 'should include additional information in the log' do
expect(logger).to receive('info') do |arguments|
expect(arguments).to have_key :headers
expect(arguments).to have_key :ip
expect(arguments).to have_key :response
end
subject
end
end
end
it 'should filter parameters in the log' do
expect(logger).to receive('info') do |arguments|
expect(arguments[:params]).to eq(
'replace_me' => '[FILTERED]',
'replace_me_too' => '[FILTERED]',
'cant_touch_this' => 'should see'
)
end
parameters = {
'replace_me' => 'should not see',
'replace_me_too' => 'should not see',
'cant_touch_this' => 'should see'
}
request.post path, params: parameters
end
end
end

View file

@ -1,87 +0,0 @@
$:.unshift '.'
require 'lib/grape_logging'
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
# rspec-mocks config goes here. You can use an alternate test double
# library (such as bogus or mocha) by changing the `mock_with` option here.
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
# have no way to turn it off -- the option exists only for backwards
# compatibility in RSpec 3). It causes shared context metadata to be
# inherited by the metadata hash of host groups and examples, rather than
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
# # This allows you to limit a spec run to individual examples or groups
# # you care about by tagging them with `:focus` metadata. When nothing
# # is tagged with `:focus`, all examples get run. RSpec also provides
# # aliases for `it`, `describe`, and `context` that include `:focus`
# # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
# config.filter_run_when_matching :focus
#
# # Allows RSpec to persist some state between runs in order to support
# # the `--only-failures` and `--next-failure` CLI options. We recommend
# # you configure your source control system to ignore this file.
# config.example_status_persistence_file_path = "spec/examples.txt"
#
# # Limits the available syntax to the non-monkey patched syntax that is
# # recommended. For more details, see:
# # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
# config.disable_monkey_patching!
#
# # This setting enables warnings. It's recommended, but in some cases may
# # be too noisy due to issues in dependencies.
# config.warnings = true
#
# # Many RSpec users commonly either run the entire suite or an individual
# # file, and it's useful to allow more verbose output when running an
# # individual spec file.
# if config.files_to_run.one?
# # Use the documentation formatter for detailed output,
# # unless a formatter has already been configured
# # (e.g. via a command-line flag).
# config.default_formatter = 'doc'
# end
#
# # Print the 10 slowest examples and example groups at the
# # end of the spec run, to help surface which specs are running
# # particularly slow.
# config.profile_examples = 10
#
# # Run specs in random order to surface order dependencies. If you find an
# # order dependency and want to debug it, you can fix the order by providing
# # the seed, which is printed after each run.
# # --seed 1234
# config.order = :random
#
# # Seed global randomization in this process using the `--seed` CLI option.
# # Setting this allows you to use `--seed` to deterministically reproduce
# # test failures related to randomization by passing the same `--seed` value
# # as the one that triggered the failure.
# Kernel.srand config.seed
end