├── .byebug_history ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── grape-middleware-logger.gemspec ├── lib └── grape │ └── middleware │ ├── logger.rb │ └── logger │ └── railtie.rb └── spec ├── factories.rb ├── fixtures └── rails_app.rb ├── integration └── lib │ └── grape │ └── middleware │ ├── headers_option_spec.rb │ └── logger_spec.rb ├── integration_rails └── lib │ └── grape │ └── middleware │ └── logger_spec.rb ├── lib └── grape │ └── middleware │ ├── headers_option_spec.rb │ └── logger_spec.rb ├── rails_helper.rb └── spec_helper.rb /.byebug_history: -------------------------------------------------------------------------------- 1 | exit 2 | c 3 | log_statements.compact.delete_if(&:empty?).each(&:strip!) 4 | log_statements.compact.delete_if(&:empty?).each(&:lstrip) 5 | log_statements.compact.delete_if(&:empty?).each(&:strip) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.gem 16 | .ruby-version 17 | .ruby-gemset -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.8 4 | - 2.4.5 5 | - 2.5.3 6 | before_install: 7 | - gem install bundler 8 | - gem install mime-types 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.12.0 (5/13/2019) 2 | ================== 3 | * [#25] Support Rails 6.0.0 (Thanks [@serggl](https://github.com/serggl)) 4 | 5 | 1.9.0 (7/7/2017) 6 | ================== 7 | * [#19] Support Grape 1.0.0 (Thanks [@badlamer](https://github.com/badlamer)) 8 | 9 | 1.8.0 (4/22/2017) 10 | ================== 11 | * [#17] Add a `:headers` option, which can be either `:all` or an array of strings. (Thanks [@yamamotok](https://github.com/yamamotok)) 12 | 13 | 1.7.1 (11/1/2016) 14 | ================== 15 | * Log the error class name (https://github.com/ridiculous/grape-middleware-logger/pull/13) 16 | 17 | 1.7.0 (8/2/2016) 18 | ================== 19 | 20 | * Bump Grape dependency to 0.17 21 | * Encourage `insert_after` when mounting to properly include query and post data 22 | 23 | ```ruby 24 | insert_after Grape::Middleware::Formatter, Grape::Middleware::Logger 25 | ``` 26 | 27 | 1.6.0 (4/4/2016) 28 | ================== 29 | 30 | * Can use default rake command to run test suite 31 | * Fix [#4](https://github.com/ridiculous/grape-middleware-logger/issues/4), missing JSON parameters from POST requests 32 | * Grape::Middleware::Formatter#before is Ruby 1.9.3 friendly 33 | 34 | 1.5.1 (12/15/2015) 35 | ================== 36 | 37 | * Refactor logger conditional to use coercion and parallel assignment 38 | 39 | 40 | 1.5.0 (12/12/2015) 41 | ================== 42 | 43 | * Use Railtie to setup default configuration for Rails apps 44 | * Stop logging the namespace 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Grape::Middleware::logger Code of Conduct 2 | 3 | The Grape::Middleware::logger project strongly values contributors from anywhere, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, ethnicity, age, religion, or nationality. As a result, the Grape::Middleware::logger team has agreed to and enforces this code of conduct in order to provide a harassment-free experience for everyone who participates in the development of Grape::Middleware::logger. 4 | 5 | ### Summary 6 | 7 | Harassment in code and discussion or violation of physical boundaries is completely unacceptable anywhere in the Grape::Middleware::logger project’s codebases, issue trackers, chat rooms, mailing lists, meetups, and any other events. Violators will be warned and then blocked or banned by the core team at or before the 3rd violation. 8 | 9 | ### In detail 10 | 11 | Harassment includes offensive verbal comments related to level of experience, gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, ethnicity, age, religion, nationality, the use of sexualized language or imagery, deliberate intimidation, stalking, sustained disruption, and unwelcome sexual attention. 12 | 13 | Individuals asked to stop any harassing behavior are expected to comply immediately. 14 | 15 | Maintainers, including the core team, are also subject to the anti-harassment policy. 16 | 17 | If anyone engages in abusive, harassing, or otherwise unacceptable behavior, including maintainers, we may take appropriate action, up to and including warning the offender, deletion of comments, removal from the project’s codebase and communication systems, and escalation to Github support. 18 | 19 | If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact a member of the core team immediately. 20 | 21 | We expect everyone to follow these rules anywhere in the logger project’s codebases, issue trackers, IRC channel, group chat, and mailing lists. 22 | 23 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 24 | 25 | Finally, don't forget that it is human to make mistakes! We all do. Let’s work together to help each other, resolve issues, and learn from the mistakes that we will all inevitably make from time to time. 26 | 27 | 28 | ### License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in grape-middleware-logger.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'rake' 8 | gem "factory_girl", "~> 4.0" 9 | gem "rails", "~> 5.2" 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ryan Buckley 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A logger for [Grape](https://github.com/ruby-grape/grape) apps 2 | [![Code Climate](https://codeclimate.com/github/ridiculous/grape-middleware-logger/badges/gpa.svg)](https://codeclimate.com/github/ridiculous/grape-middleware-logger) [![Gem Version](https://badge.fury.io/rb/grape-middleware-logger.svg)](http://badge.fury.io/rb/grape-middleware-logger) 3 | [![Build Status](https://travis-ci.org/ridiculous/grape-middleware-logger.svg)](https://travis-ci.org/ridiculous/grape-middleware-logger) 4 | 5 | Logs: 6 | * Request path 7 | * Parameters 8 | * Endpoint class name and handler 9 | * Response status 10 | * Duration of the request 11 | * Exceptions 12 | * Error responses from `error!` 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | ```ruby 19 | gem 'grape', '>= 0.17' 20 | gem 'grape-middleware-logger' 21 | ``` 22 | 23 | ## Usage 24 | ```ruby 25 | require 'grape' 26 | require 'grape/middleware/logger' 27 | 28 | class API < Grape::API 29 | # @note Make sure this is above your first +mount+ 30 | insert_after Grape::Middleware::Formatter, Grape::Middleware::Logger 31 | end 32 | ``` 33 | 34 | Server requests will be logged to STDOUT by default. 35 | 36 | ## Example output 37 | GET 38 | ``` 39 | Started GET "/v1/reports/101" at 2015-12-11 15:40:51 -0800 40 | Processing by ReportsAPI/reports/:id 41 | Parameters: {"id"=>"101"} 42 | Completed 200 in 6.29ms 43 | ``` 44 | POST 45 | ``` 46 | Started POST "/v1/reports" at 2015-12-11 15:42:33 -0800 47 | Processing by ReportsAPI/reports 48 | Parameters: {"name"=>"foo", "password"=>"[FILTERED]"} 49 | Error: {:error=>"undefined something something bad", :detail=>"Whoops"} 50 | Completed 422 in 6.29ms 51 | ``` 52 | 53 | ## Customization 54 | 55 | The middleware logger can be customized with the following options: 56 | 57 | * The `:logger` option can be any object that responds to `.info(String)` 58 | * The `:condensed` option configures the log output to be on one line instead of multiple. It accepts `true` or `false`. The default configuration is `false` 59 | * The `:filter` option can be any object that responds to `.filter(Hash)` and returns a hash. 60 | * The `:headers` option can be either `:all` or array of strings. 61 | + If `:all`, all request headers will be output. 62 | + If array, output will be filtered by names in the array. (case-insensitive) 63 | 64 | For example: 65 | 66 | ```ruby 67 | insert_after Grape::Middleware::Formatter, Grape::Middleware::Logger, { 68 | logger: Logger.new(STDERR), 69 | condensed: true, 70 | filter: Class.new { def filter(opts) opts.reject { |k, _| k.to_s == 'password' } end }.new, 71 | headers: %w(version cache-control) 72 | } 73 | ``` 74 | 75 | ## Using Rails? 76 | `Rails.logger` and `Rails.application.config.filter_parameters` will be used automatically as the default logger and 77 | param filterer, respectively. This behavior can be overridden by passing the `:logger` or 78 | `:filter` option when mounting. 79 | 80 | You may want to disable Rails logging for API endpoints, so that the logging doesn't double-up. You can achieve this 81 | by switching around some middleware. For example: 82 | 83 | ```ruby 84 | # config/application.rb 85 | config.middleware.swap 'Rails::Rack::Logger', 'SelectiveLogger' 86 | 87 | # config/initializers/selective_logger.rb 88 | class SelectiveLogger 89 | def initialize(app) 90 | @app = app 91 | end 92 | 93 | def call(env) 94 | if env['PATH_INFO'] =~ %r{^/api} 95 | @app.call(env) 96 | else 97 | Rails::Rack::Logger.new(@app).call(env) 98 | end 99 | end 100 | end 101 | ``` 102 | 103 | ## Rack 104 | 105 | If you're using the `rackup` command to run your server in development, pass the `-q` flag to silence the default rack logger. 106 | 107 | ## Credits 108 | 109 | Big thanks to jadent's question/answer on [stackoverflow](http://stackoverflow.com/questions/25048163/grape-using-error-and-grapemiddleware-after-callback) 110 | for easily logging error responses. Borrowed some motivation from the [grape_logging](https://github.com/aserafin/grape_logging) gem 111 | and would love to see these two consolidated at some point. 112 | 113 | ## Contributing 114 | 115 | 1. Fork it ( https://github.com/ridiculous/grape-middleware-logger/fork ) 116 | 2. Create your feature branch (`git checkout -b my-new-feature`) 117 | 3. Commit your changes (`git commit -am 'Add some feature'`) 118 | 4. Push to the branch (`git push origin my-new-feature`) 119 | 5. Create a new Pull Request 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |config| 5 | config.pattern = 'spec/lib/**/*_spec.rb' 6 | end 7 | 8 | RSpec::Core::RakeTask.new(:integration) do |config| 9 | config.pattern = 'spec/integration/**/*_spec.rb' 10 | end 11 | 12 | RSpec::Core::RakeTask.new(:integration_rails) do |config| 13 | config.pattern = 'spec/integration_rails/**/*_spec.rb' 14 | end 15 | 16 | task default: [:spec, :integration, :integration_rails] 17 | -------------------------------------------------------------------------------- /grape-middleware-logger.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'grape-middleware-logger' 5 | spec.version = '1.12.0' 6 | spec.platform = Gem::Platform::RUBY 7 | spec.authors = ['Ryan Buckley'] 8 | spec.email = ['arebuckley@gmail.com'] 9 | spec.summary = %q{A logger for the Grape framework} 10 | spec.description = %q{Logging middleware for the Grape framework, similar to what Rails offers} 11 | spec.homepage = 'https://github.com/ridiculous/grape-middleware-logger' 12 | spec.license = 'MIT' 13 | 14 | spec.files = `git ls-files -z`.split("\x0").grep(%r{^lib/|gemspec}) 15 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 16 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 17 | spec.require_paths = ['lib'] 18 | 19 | spec.post_install_message = %q{ 20 | 21 | Grape::Middleware::Logger 1.7+ should be mounted with +insert_after+ to properly include POST params: 22 | 23 | insert_after Grape::Middleware::Formatter, Grape::Middleware::Logger 24 | 25 | } 26 | 27 | spec.add_dependency 'grape', '>= 0.17' 28 | spec.add_development_dependency 'mime-types', '~> 2' 29 | spec.add_development_dependency 'rake', '>= 12.3.3' 30 | spec.add_development_dependency 'rspec', '>= 3.2', '< 4' 31 | end 32 | -------------------------------------------------------------------------------- /lib/grape/middleware/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'grape' 3 | 4 | class Grape::Middleware::Logger < Grape::Middleware::Globals 5 | BACKSLASH = '/'.freeze 6 | 7 | attr_reader :logger 8 | 9 | class << self 10 | attr_accessor :logger, :filter, :headers, :condensed 11 | 12 | def default_logger 13 | default = Logger.new(STDOUT) 14 | default.formatter = ->(*args) { args.last.to_s << "\n".freeze } 15 | default 16 | end 17 | end 18 | 19 | def initialize(_, options = {}) 20 | super 21 | @options[:filter] ||= self.class.filter 22 | @options[:headers] ||= self.class.headers 23 | @options[:condensed] ||= false 24 | @logger = options[:logger] || self.class.logger || self.class.default_logger 25 | end 26 | 27 | def before 28 | start_time 29 | # sets env['grape.*'] 30 | super 31 | 32 | log_statements = [ 33 | '', 34 | %Q(Started %s "%s" at %s) % [ 35 | env[Grape::Env::GRAPE_REQUEST].request_method, 36 | env[Grape::Env::GRAPE_REQUEST].path, 37 | start_time.to_s 38 | ], 39 | %Q(Processing by #{processed_by}), 40 | %Q( Parameters: #{parameters})] 41 | 42 | log_statements.append(%Q( Headers: #{headers})) if @options[:headers] 43 | log_info(log_statements) 44 | end 45 | 46 | # @note Error and exception handling are required for the +after+ hooks 47 | # Exceptions are logged as a 500 status and re-raised 48 | # Other "errors" are caught, logged and re-thrown 49 | def call!(env) 50 | @env = env 51 | before 52 | error = catch(:error) do 53 | begin 54 | @app_response = @app.call(@env) 55 | rescue => e 56 | after_exception(e) 57 | raise e 58 | end 59 | nil 60 | end 61 | if error 62 | after_failure(error) 63 | throw(:error, error) 64 | else 65 | status, _, _ = *@app_response 66 | after(status) 67 | end 68 | @app_response 69 | end 70 | 71 | def after(status) 72 | log_info( 73 | [ 74 | "Completed #{status} in #{((Time.now - start_time) * 1000).round(2)}ms", 75 | '' 76 | ] 77 | ) 78 | end 79 | 80 | # 81 | # Helpers 82 | # 83 | 84 | def after_exception(e) 85 | logger.info %Q( #{e.class.name}: #{e.message}) 86 | after(500) 87 | end 88 | 89 | def after_failure(error) 90 | logger.info %Q( Error: #{error[:message]}) if error[:message] 91 | after(error[:status]) 92 | end 93 | 94 | def parameters 95 | request_params = env[Grape::Env::GRAPE_REQUEST_PARAMS].to_hash 96 | request_params.merge! env[Grape::Env::RACK_REQUEST_FORM_HASH] if env[Grape::Env::RACK_REQUEST_FORM_HASH] 97 | request_params.merge! env['action_dispatch.request.request_parameters'] if env['action_dispatch.request.request_parameters'] 98 | if @options[:filter] 99 | @options[:filter].filter(request_params) 100 | else 101 | request_params 102 | end 103 | end 104 | 105 | def headers 106 | request_headers = env[Grape::Env::GRAPE_REQUEST_HEADERS].to_hash 107 | return Hash[request_headers.sort] if @options[:headers] == :all 108 | 109 | headers_needed = Array(@options[:headers]) 110 | result = {} 111 | headers_needed.each do |need| 112 | result.merge!(request_headers.select { |key, value| need.to_s.casecmp(key).zero? }) 113 | end 114 | Hash[result.sort] 115 | end 116 | 117 | def start_time 118 | @start_time ||= Time.now 119 | end 120 | 121 | def processed_by 122 | endpoint = env[Grape::Env::API_ENDPOINT] 123 | result = [] 124 | if endpoint.namespace == BACKSLASH 125 | result << '' 126 | else 127 | result << endpoint.namespace 128 | end 129 | result.concat endpoint.options[:path].map { |path| path.to_s.sub(BACKSLASH, '') } 130 | endpoint.options[:for].to_s << result.join(BACKSLASH) 131 | end 132 | 133 | def log_info(log_statements=[]) 134 | if @options[:condensed] 135 | logger.info log_statements.compact.delete_if(&:empty?).each(&:strip!).join(" - ") 136 | else 137 | log_statements.each { |log_statement| logger.info log_statement } 138 | end 139 | end 140 | end 141 | 142 | require_relative 'logger/railtie' if defined?(Rails) 143 | -------------------------------------------------------------------------------- /lib/grape/middleware/logger/railtie.rb: -------------------------------------------------------------------------------- 1 | class Grape::Middleware::Logger::Railtie < Rails::Railtie 2 | options = Rails::VERSION::MAJOR < 5 ? { after: :load_config_initializers } : {} 3 | initializer 'grape.middleware.logger', options do 4 | Grape::Middleware::Logger.logger = Rails.application.config.logger || Rails.logger.presence 5 | parameter_filter_class = if Rails::VERSION::MAJOR >= 6 6 | ActiveSupport::ParameterFilter 7 | else 8 | ActionDispatch::Http::ParameterFilter 9 | end 10 | Grape::Middleware::Logger.filter = parameter_filter_class.new Rails.application.config.filter_parameters 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | class ExpectedEnv < Hash 3 | attr_accessor :grape_request, :params, :post_params, :rails_post_params, :grape_endpoint 4 | end 5 | 6 | class ParamFilter 7 | attr_accessor :list 8 | 9 | def filter(opts) 10 | opts.each_pair { |key, val| val[0..-1] = '[FILTERED]' if list.include?(key) } 11 | end 12 | end 13 | 14 | class TestAPI 15 | end 16 | 17 | class App 18 | attr_accessor :response 19 | 20 | def call(_env) 21 | response 22 | end 23 | end 24 | 25 | factory :param_filter do 26 | list %w[password secret] 27 | end 28 | 29 | require 'rack/rewindable_input' 30 | 31 | factory :expected_env do 32 | grape_request { build :grape_request } 33 | grape_endpoint { build(:grape_endpoint) } 34 | params { grape_request.params } 35 | headers { grape_request.headers } 36 | post_params { { 'secret' => 'key', 'customer' => [] } } 37 | rails_post_params { { 'name' => 'foo', 'password' => 'access' } } 38 | other_env_params { {} } 39 | 40 | initialize_with do 41 | new.merge( 42 | 'REQUEST_METHOD' => 'POST', 43 | 'PATH_INFO' => '/api/1.0/users', 44 | 'action_dispatch.request.request_parameters' => rails_post_params, 45 | Grape::Env::GRAPE_REQUEST => grape_request, 46 | Grape::Env::GRAPE_REQUEST_PARAMS => params, 47 | Grape::Env::GRAPE_REQUEST_HEADERS => headers, 48 | Grape::Env::RACK_REQUEST_FORM_HASH => post_params, 49 | Grape::Env::API_ENDPOINT => grape_endpoint 50 | ).merge(other_env_params) 51 | end 52 | 53 | trait :prefixed_basic_headers do 54 | other_env_params { { 55 | 'HTTP_VERSION' => 'HTTP/1.1', 56 | 'HTTP_CACHE_CONTROL' => 'max-age=0', 57 | 'HTTP_USER_AGENT' => 'Mozilla/5.0', 58 | 'HTTP_ACCEPT_LANGUAGE' => 'en-US' 59 | } } 60 | end 61 | 62 | end 63 | 64 | factory :grape_endpoint, class: Grape::Endpoint do 65 | settings { Grape::Util::InheritableSetting.new } 66 | options { 67 | { 68 | path: [:users], 69 | method: 'get', 70 | for: TestAPI 71 | } 72 | } 73 | 74 | initialize_with { new(settings, options) } 75 | 76 | trait :complex do 77 | options { 78 | { 79 | path: ['/users/:name/profile'], 80 | method: 'put', 81 | for: TestAPI 82 | } 83 | } 84 | end 85 | end 86 | 87 | factory :namespaced_endpoint, parent: :grape_endpoint do 88 | initialize_with do 89 | new(settings, options).tap do |me| 90 | me.namespace_stackable(:namespace, Grape::Namespace.new('/admin', {})) 91 | end 92 | end 93 | end 94 | 95 | factory :app_response, class: Rack::Response do 96 | initialize_with { new('Hello World', 200, {}) } 97 | end 98 | 99 | factory :grape_request, class: OpenStruct do 100 | headers { {} } 101 | 102 | initialize_with { 103 | new(request_method: 'POST', path: '/api/1.0/users', headers: headers, params: { 'id' => '101001' }) 104 | } 105 | 106 | trait :basic_headers do 107 | headers { { 108 | 'Version' => 'HTTP/1.1', 109 | 'Cache-Control' => 'max-age=0', 110 | 'User-Agent' => 'Mozilla/5.0', 111 | 'Accept-Language' => 'en-US' 112 | } } 113 | end 114 | end 115 | 116 | factory :app do 117 | response { build :app_response } 118 | end 119 | 120 | end 121 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | class RailsApp < Rails::Application 3 | RailsLogger = Class.new Logger 4 | config.logger = RailsLogger.new(Tempfile.new '') 5 | config.filter_parameters += [:password] 6 | config.eager_load = false 7 | end 8 | -------------------------------------------------------------------------------- /spec/integration/lib/grape/middleware/headers_option_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Grape::Middleware::Logger, type: :integration do 4 | let(:app) { build :app } 5 | 6 | subject { described_class.new(app, options) } 7 | 8 | let(:grape_endpoint) { build(:grape_endpoint) } 9 | let(:env) { build(:expected_env, :prefixed_basic_headers, grape_endpoint: grape_endpoint) } 10 | 11 | context ':all option is set to option headers' do 12 | let(:options) { { 13 | filter: build(:param_filter), 14 | headers: :all, 15 | logger: Logger.new(Tempfile.new('logger')) 16 | } } 17 | it 'all headers will be shown, headers will be sorted by name' do 18 | expect(subject.logger).to receive(:info).with '' 19 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 20 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 21 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 22 | expect(subject.logger).to receive(:info).with %Q( Headers: {"Accept-Language"=>"en-US", "Cache-Control"=>"max-age=0", "User-Agent"=>"Mozilla/5.0", "Version"=>"HTTP/1.1"}) 23 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 24 | expect(subject.logger).to receive(:info).with '' 25 | subject.call!(env) 26 | end 27 | end 28 | 29 | context 'list of names ["User-Agent", "Cache-Control"] is set to option headers' do 30 | let(:options) { { 31 | filter: build(:param_filter), 32 | headers: %w(User-Agent Cache-Control), 33 | logger: Logger.new(Tempfile.new('logger')) 34 | } } 35 | it 'two headers will be shown' do 36 | expect(subject.logger).to receive(:info).with '' 37 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 38 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 39 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 40 | expect(subject.logger).to receive(:info).with %Q( Headers: {"Cache-Control"=>"max-age=0", "User-Agent"=>"Mozilla/5.0"}) 41 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 42 | expect(subject.logger).to receive(:info).with '' 43 | subject.call!(env) 44 | end 45 | end 46 | 47 | context 'a single string "Cache-Control" is set to option headers' do 48 | let(:options) { { 49 | filter: build(:param_filter), 50 | headers: 'Cache-Control', 51 | logger: Logger.new(Tempfile.new('logger')) 52 | } } 53 | it 'only Cache-Control header will be shown' do 54 | expect(subject.logger).to receive(:info).with '' 55 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 56 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 57 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 58 | expect(subject.logger).to receive(:info).with %Q( Headers: {"Cache-Control"=>"max-age=0"}) 59 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 60 | expect(subject.logger).to receive(:info).with '' 61 | subject.call!(env) 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/integration/lib/grape/middleware/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Grape::Middleware::Logger, type: :integration do 4 | let(:app) { build :app } 5 | let(:options) { { filter: build(:param_filter), logger: Logger.new(Tempfile.new('logger')) } } 6 | 7 | subject { described_class.new(app, options) } 8 | 9 | let(:app_response) { build :app_response } 10 | let(:grape_request) { build :grape_request } 11 | let(:grape_endpoint) { build(:grape_endpoint) } 12 | let(:env) { build(:expected_env, grape_endpoint: grape_endpoint) } 13 | 14 | context 'when the option[:condensed] is false' do 15 | let(:options) { { filter: build(:param_filter), logger: Logger.new(Tempfile.new('logger')), condensed: false } } 16 | 17 | it 'logs all parts of the request on multiple lines' do 18 | expect(subject.logger).to receive(:info).with '' 19 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 20 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 21 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 22 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 23 | expect(subject.logger).to receive(:info).with '' 24 | subject.call!(env) 25 | end 26 | end 27 | 28 | context 'when the options[:condensed is true' do 29 | let(:options) { { filter: build(:param_filter), logger: Logger.new(Tempfile.new('logger')), condensed: true } } 30 | 31 | it 'logs all parts of the request on one line' do 32 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time} - Processing by TestAPI/users - Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 33 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 34 | subject.call!(env) 35 | end 36 | end 37 | 38 | context 'when an exception occurs' do 39 | it 'logs all parts of the request including the error class' do 40 | expect(subject.logger).to receive(:info).with '' 41 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 42 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 43 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"[FILTERED]", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 44 | expect(subject.logger).to receive(:info).with %Q( ArgumentError: Whoops) 45 | expect(subject.logger).to receive(:info).with /Completed 500 in \d+\.\d+ms/ 46 | expect(subject.logger).to receive(:info).with '' 47 | expect(subject.app).to receive(:call).and_raise(ArgumentError, 'Whoops') 48 | expect { 49 | subject.call!(env) 50 | }.to raise_error(ArgumentError) 51 | end 52 | end 53 | 54 | describe 'the "processing by" section' do 55 | before { subject.call!(env) } 56 | 57 | context 'namespacing' do 58 | let(:grape_endpoint) { build(:namespaced_endpoint) } 59 | 60 | it 'ignores the namespacing' do 61 | expect(subject.processed_by).to eq 'TestAPI/admin/users' 62 | end 63 | 64 | context 'with more complex route' do 65 | let(:grape_endpoint) { build(:namespaced_endpoint, :complex) } 66 | 67 | it 'only escapes the first slash and leaves the rest of the untouched' do 68 | expect(subject.processed_by).to eq 'TestAPI/admin/users/:name/profile' 69 | end 70 | end 71 | end 72 | 73 | context 'with more complex route' do 74 | let(:grape_endpoint) { build(:grape_endpoint, :complex) } 75 | 76 | it 'only escapes the first slash and leaves the rest of the untouched' do 77 | expect(subject.processed_by).to eq 'TestAPI/users/:name/profile' 78 | end 79 | end 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /spec/integration_rails/lib/grape/middleware/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Grape::Middleware::Logger, type: :rails_integration do 4 | let(:app) { build :app } 5 | let(:options) { {} } 6 | 7 | subject { described_class.new(app, options) } 8 | 9 | let(:app_response) { build :app_response } 10 | let(:grape_request) { build :grape_request } 11 | let(:grape_endpoint) { build(:grape_endpoint) } 12 | let(:env) { build(:expected_env, grape_endpoint: grape_endpoint) } 13 | 14 | describe '#logger' do 15 | context 'when @options[:logger] is nil' do 16 | context 'when Rails.application.config.logger is defined' do 17 | it 'uses the Rails logger' do 18 | expect(subject.logger).to be_present 19 | expect(subject.logger).to be Rails.application.config.logger 20 | expect(subject.logger.formatter).to be_nil 21 | end 22 | end 23 | 24 | context 'when the class logger is nil' do 25 | before { described_class.logger = nil } 26 | 27 | it 'uses the default logger' do 28 | expect(subject.logger).to be_present 29 | expect(subject.logger).to_not be Rails.application.config.logger 30 | expect(subject.logger).to be_a(Logger) 31 | expect(subject.logger.formatter.call('foo')).to eq "foo\n" 32 | end 33 | end 34 | end 35 | 36 | context 'when @options[:logger] is set' do 37 | let(:options) { { logger: Object.new } } 38 | 39 | it 'returns the logger object' do 40 | expect(subject.logger).to eq options[:logger] 41 | end 42 | end 43 | end 44 | 45 | context 'when the option[:condensed] is false' do 46 | let(:options) { { condensed: false } } 47 | 48 | it 'logs all parts of the request on multiple lines' do 49 | expect(subject.logger).to receive(:info).with '' 50 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time}) 51 | expect(subject.logger).to receive(:info).with %Q(Processing by TestAPI/users) 52 | expect(subject.logger).to receive(:info).with %Q( Parameters: {"id"=>"101001", "secret"=>"key", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 53 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 54 | expect(subject.logger).to receive(:info).with '' 55 | subject.call!(env) 56 | end 57 | end 58 | 59 | context 'when the option[:condensed] is true' do 60 | let(:options) { { condensed: true } } 61 | it 'logs all parts of the request on one line' do 62 | expect(subject.logger).to receive(:info).with %Q(Started POST "/api/1.0/users" at #{subject.start_time} - Processing by TestAPI/users - Parameters: {"id"=>"101001", "secret"=>"key", "customer"=>[], "name"=>"foo", "password"=>"[FILTERED]"}) 63 | expect(subject.logger).to receive(:info).with /Completed 200 in \d+.\d+ms/ 64 | subject.call!(env) 65 | end 66 | end 67 | 68 | describe 'the "processing by" section' do 69 | before { subject.call!(env) } 70 | 71 | context 'namespacing' do 72 | let(:grape_endpoint) { build(:namespaced_endpoint) } 73 | 74 | it 'ignores the namespacing' do 75 | expect(subject.processed_by).to eq 'TestAPI/admin/users' 76 | end 77 | 78 | context 'with more complex route' do 79 | let(:grape_endpoint) { build(:namespaced_endpoint, :complex) } 80 | 81 | it 'only escapes the first slash and leaves the rest of the untouched' do 82 | expect(subject.processed_by).to eq 'TestAPI/admin/users/:name/profile' 83 | end 84 | end 85 | end 86 | 87 | context 'with more complex route' do 88 | let(:grape_endpoint) { build(:grape_endpoint, :complex) } 89 | 90 | it 'only escapes the first slash and leaves the rest of the untouched' do 91 | expect(subject.processed_by).to eq 'TestAPI/users/:name/profile' 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/lib/grape/middleware/headers_option_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Grape::Middleware::Logger do 4 | let(:app) { double('app') } 5 | 6 | subject { described_class.new(app, options) } 7 | 8 | describe '#headers' do 9 | let(:grape_request) { build :grape_request, :basic_headers } 10 | let(:env) { build :expected_env, grape_request: grape_request } 11 | 12 | before { subject.instance_variable_set(:@env, env) } 13 | 14 | context 'when @options[:headers] has a symbol :all' do 15 | let(:options) { { headers: :all, logger: Object.new } } 16 | it 'all request headers should be retrieved' do 17 | expect(subject.headers.fetch('Accept-Language')).to eq('en-US') 18 | expect(subject.headers.fetch('Cache-Control')).to eq('max-age=0') 19 | expect(subject.headers.fetch('User-Agent')).to eq('Mozilla/5.0') 20 | expect(subject.headers.fetch('Version')).to eq('HTTP/1.1') 21 | end 22 | end 23 | 24 | context 'when @options[:headers] is a string "user-agent"' do 25 | let(:options) { { headers: 'user-agent', logger: Object.new } } 26 | it 'only "User-Agent" should be retrieved' do 27 | expect(subject.headers.fetch('User-Agent')).to eq('Mozilla/5.0') 28 | expect(subject.headers.length).to eq(1) 29 | end 30 | end 31 | 32 | context 'when @options[:headers] is an array of ["user-agent", "Cache-Control", "Unknown"]' do 33 | let(:options) { { headers: %w(user-agent Cache-Control Unknown), logger: Object.new } } 34 | it '"User-Agent" and "Cache-Control" should be retrieved' do 35 | expect(subject.headers.fetch('Cache-Control')).to eq('max-age=0') 36 | expect(subject.headers.fetch('User-Agent')).to eq('Mozilla/5.0') 37 | end 38 | it '"Unknown" name does not make any effect' do 39 | expect(subject.headers.length).to eq(2) 40 | end 41 | end 42 | end 43 | 44 | describe '#headers if no request header' do 45 | let(:env) { build :expected_env } 46 | before { subject.instance_variable_set(:@env, env) } 47 | 48 | context 'when @options[:headers] is set, but no request header is there' do 49 | let(:options) { { headers: %w(user-agent Cache-Control), logger: Object.new } } 50 | it 'subject.headers should return empty hash' do 51 | expect(subject.headers.length).to eq(0) 52 | end 53 | end 54 | end 55 | 56 | end 57 | 58 | -------------------------------------------------------------------------------- /spec/lib/grape/middleware/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Grape::Middleware::Logger do 4 | let(:app) { double('app') } 5 | let(:options) { { filter: build(:param_filter), logger: Object.new } } 6 | 7 | subject { described_class.new(app, options) } 8 | 9 | let(:app_response) { build :app_response } 10 | let(:grape_request) { build :grape_request } 11 | let(:env) { build :expected_env } 12 | 13 | describe '#call!' do 14 | context 'when calling the app results in an error response' do 15 | let(:error) { { status: 400 } } 16 | 17 | it 'calls +after_failure+ and rethrows the error' do 18 | expect(app).to receive(:call).with(env).and_throw(:error, error) 19 | expect(subject).to receive(:before) 20 | expect(subject).to receive(:after_failure).with(error) 21 | expect(subject).to receive(:throw).with(:error, error) 22 | subject.call!(env) 23 | end 24 | end 25 | 26 | context 'when there is no error' do 27 | it 'calls +after+ with the correct status' do 28 | expect(app).to receive(:call).with(env).and_return(app_response) 29 | expect(subject).to receive(:before) 30 | expect(subject).to receive(:after).with(200) 31 | subject.call!(env) 32 | end 33 | 34 | it 'returns the @app_response' do 35 | expect(app).to receive(:call).with(env).and_return(app_response) 36 | allow(subject).to receive(:before) 37 | allow(subject).to receive(:after) 38 | expect(subject.call!(env)).to eq app_response 39 | end 40 | end 41 | 42 | context 'when calling the app results in an array response' do 43 | let(:app_response) { [401, {}, []] } 44 | 45 | it 'calls +after+ with the correct status' do 46 | expect(app).to receive(:call).with(env).and_return(app_response) 47 | expect(subject).to receive(:before) 48 | expect(subject).to receive(:after).with(401) 49 | subject.call!(env) 50 | end 51 | 52 | it 'returns the @app_response' do 53 | expect(app).to receive(:call).with(env).and_return(app_response) 54 | allow(subject).to receive(:before) 55 | allow(subject).to receive(:after) 56 | expect(subject.call!(env)).to eq app_response 57 | end 58 | end 59 | end 60 | 61 | describe '#after_failure' do 62 | let(:error) { { status: 403 } } 63 | 64 | it 'calls +after+ with the :status' do 65 | expect(subject).to receive(:after).with(403) 66 | subject.after_failure(error) 67 | end 68 | 69 | context 'when :message is set in the error object' do 70 | let(:error) { { message: 'Oops, not found' } } 71 | 72 | it 'logs the error message' do 73 | allow(subject).to receive(:after) 74 | expect(subject.logger).to receive(:info).with(Regexp.new(error[:message])) 75 | subject.after_failure(error) 76 | end 77 | end 78 | end 79 | 80 | describe '#parameters' do 81 | before { subject.instance_variable_set(:@env, env) } 82 | 83 | context 'when @options[:filter] is set' do 84 | it 'calls +filter+ with the raw parameters' do 85 | expect(subject.options[:filter]).to receive(:filter).with({ "id" => '101001', "secret" => "key", "customer" => [], "name"=>"foo", "password"=>"access" }) 86 | subject.parameters 87 | end 88 | 89 | it 'returns the filtered results' do 90 | expect(subject.parameters).to eq({ "id" => '101001', "secret" => "[FILTERED]", "customer" => [], "name"=>"foo", "password"=>"[FILTERED]" }) 91 | end 92 | end 93 | 94 | context 'when @options[:filter] is nil' do 95 | let(:options) { {} } 96 | 97 | it 'returns the params extracted out of @env' do 98 | expect(subject.parameters).to eq({ "id" => '101001', "secret" => "key", "customer" => [], "name"=>"foo", "password"=>"access" }) 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require_relative 'fixtures/rails_app' 3 | require 'spec_helper' 4 | RailsApp.initialize! 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | 4 | require 'ostruct' 5 | require 'factory_girl' 6 | require 'grape/middleware/logger' 7 | 8 | FactoryGirl.find_definitions 9 | 10 | RSpec.configure do |config| 11 | config.raise_errors_for_deprecations! 12 | config.include FactoryGirl::Syntax::Methods 13 | end 14 | --------------------------------------------------------------------------------