├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── grape_logging.gemspec ├── lib ├── grape_logging.rb └── grape_logging │ ├── formatters │ ├── default.rb │ ├── json.rb │ ├── lograge.rb │ ├── logstash.rb │ └── rails.rb │ ├── loggers │ ├── base.rb │ ├── client_env.rb │ ├── filter_parameters.rb │ ├── request_headers.rb │ └── response.rb │ ├── middleware │ └── request_logger.rb │ ├── multi_io.rb │ ├── reporters │ ├── active_support_reporter.rb │ └── logger_reporter.rb │ ├── timings.rb │ ├── util │ └── parameter_filter.rb │ └── version.rb └── spec ├── lib └── grape_logging │ ├── formatters │ └── rails_spec.rb │ ├── loggers │ ├── client_env_spec.rb │ ├── filter_parameters_spec.rb │ ├── request_headers_spec.rb │ └── response_spec.rb │ └── middleware │ └── request_logger_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .rspec 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.1 4 | cache: bundler 5 | script: 6 | - bundle exec rspec -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in grape_logging.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 aserafin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grape_logging 2 | 3 | [![Code Climate](https://codeclimate.com/github/aserafin/grape_logging/badges/gpa.svg)](https://codeclimate.com/github/aserafin/grape_logging) 4 | [![Build Status](https://travis-ci.org/aserafin/grape_logging.svg?branch=master)](https://travis-ci.org/aserafin/grape_logging) 5 | 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | gem 'grape_logging' 11 | 12 | And then execute: 13 | 14 | $ bundle install 15 | 16 | Or install it yourself as: 17 | 18 | $ gem install grape_logging 19 | 20 | ## Basic Usage 21 | 22 | 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. 23 | 24 | ```ruby 25 | require 'grape_logging' 26 | logger.formatter = GrapeLogging::Formatters::Default.new 27 | insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, { logger: logger } 28 | ``` 29 | 30 | **ProTip:** If your logger doesn't support setting formatter you can remove this line - it's optional 31 | 32 | ## Features 33 | 34 | ### Log Format 35 | 36 | There are formatters provided for you, or you can provide your own. 37 | 38 | #### `GrapeLogging::Formatters::Default` 39 | 40 | [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"}} 41 | 42 | #### `GrapeLogging::Formatters::Json` 43 | 44 | ```json 45 | { 46 | "date": "2015-04-16 12:52:12+0200", 47 | "severity": "INFO", 48 | "data": { 49 | "status": 200, 50 | "time": { 51 | "total": 2.06, 52 | "db": 0.36, 53 | "view": 1.70 54 | }, 55 | "method": "PATCH", 56 | "path": "/api/endpoint", 57 | "params": { 58 | "value_1": "123", 59 | "value_2": "456" 60 | }, 61 | "host": "localhost" 62 | } 63 | } 64 | ``` 65 | 66 | #### `GrapeLogging::Formatters::Lograge` 67 | 68 | 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" 69 | 70 | #### `GrapeLogging::Formatters::Logstash` 71 | 72 | ```json 73 | { 74 | "@timestamp": "2015-04-16 12:52:12+0200", 75 | "severity": "INFO", 76 | "status": 200, 77 | "time": { 78 | "total": 2.06, 79 | "db": 0.36, 80 | "view": 1.70 81 | }, 82 | "method": "PATCH", 83 | "path": "/api/endpoint", 84 | "params": { 85 | "value_1": "123", 86 | "value_2": "456" 87 | }, 88 | "host": "localhost" 89 | } 90 | ``` 91 | 92 | #### `GrapeLogging::Formatters::Rails` 93 | 94 | Rails will print the "Started..." line: 95 | 96 | Started GET "/api/endpoint" for ::1 at 2015-04-16 12:52:12 +0200 97 | User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 98 | ... 99 | 100 | The `Rails` formatter adds the last line of the request, like a standard Rails request: 101 | 102 | Completed 200 OK in 349ms (Views: 250.1ms | DB: 98.63ms) 103 | 104 | #### Custom 105 | 106 | You can provide your own class that implements the `call` method returning a `String`: 107 | 108 | ```ruby 109 | def call(severity, datetime, _, data) 110 | ... 111 | end 112 | ``` 113 | 114 | You can change the formatter like so 115 | ```ruby 116 | class MyAPI < Grape::API 117 | use GrapeLogging::Middleware::RequestLogger, logger: logger, formatter: MyFormatter.new 118 | end 119 | ``` 120 | 121 | If you prefer some other format I strongly encourage you to do pull request with new formatter class ;) 122 | 123 | ### Customising What Is Logged 124 | 125 | You can include logging of other parts of the request / response cycle by including subclasses of `GrapeLogging::Loggers::Base` 126 | ```ruby 127 | class MyAPI < Grape::API 128 | use GrapeLogging::Middleware::RequestLogger, 129 | logger: logger, 130 | include: [ GrapeLogging::Loggers::Response.new, 131 | GrapeLogging::Loggers::FilterParameters.new, 132 | GrapeLogging::Loggers::ClientEnv.new, 133 | GrapeLogging::Loggers::RequestHeaders.new ] 134 | end 135 | ``` 136 | 137 | #### FilterParameters 138 | 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. 139 | 140 | #### ClientEnv 141 | The `ClientEnv` logger will add `ip` and user agent `ua` in your log. 142 | 143 | #### RequestHeaders 144 | The `RequestHeaders` logger will add `request headers` in your log. 145 | 146 | ### Logging to file and STDOUT 147 | 148 | You can log to file and STDOUT at the same time, you just need to assign new logger 149 | ```ruby 150 | log_file = File.open('path/to/your/logfile.log', 'a') 151 | log_file.sync = true 152 | logger Logger.new GrapeLogging::MultiIO.new(STDOUT, log_file) 153 | ``` 154 | 155 | ### Set the log level 156 | 157 | You can control the level used to log. The default is `info`. 158 | 159 | ```ruby 160 | class MyAPI < Grape::API 161 | use GrapeLogging::Middleware::RequestLogger, 162 | logger: logger, 163 | log_level: 'debug' 164 | end 165 | ``` 166 | 167 | ### Logging via Rails instrumentation 168 | 169 | 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. 170 | First, config ```grape_logging```, like that: 171 | ```ruby 172 | class MyAPI < Grape::API 173 | use GrapeLogging::Middleware::RequestLogger, 174 | instrumentation_key: 'grape_key', 175 | include: [ GrapeLogging::Loggers::Response.new, 176 | GrapeLogging::Loggers::FilterParameters.new ] 177 | end 178 | ``` 179 | 180 | and then add an initializer in your Rails project: 181 | ```ruby 182 | # config/initializers/instrumentation.rb 183 | 184 | # Subscribe to grape request and log with Rails.logger 185 | ActiveSupport::Notifications.subscribe('grape_key') do |name, starts, ends, notification_id, payload| 186 | Rails.logger.info payload 187 | end 188 | ``` 189 | 190 | The idea come from here: https://gist.github.com/teamon/e8ae16ffb0cb447e5b49 191 | 192 | ### Logging exceptions 193 | 194 | If you want to log exceptions you can do it like this 195 | ```ruby 196 | class MyAPI < Grape::API 197 | rescue_from :all do |e| 198 | MyAPI.logger.error e 199 | #do here whatever you originally planned to do :) 200 | end 201 | end 202 | ``` 203 | ## Development 204 | 205 | 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. 206 | 207 | 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). 208 | 209 | ## Contributing 210 | 211 | 1. Fork it ( https://github.com/aserafin/grape_logging/fork ) 212 | 2. Create your feature branch (`git checkout -b my-new-feature`) 213 | 3. Commit your changes (`git commit -am 'Add some feature'`) 214 | 4. Push to the branch (`git push origin my-new-feature`) 215 | 5. Create a new Pull Request 216 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'grape_logging' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /grape_logging.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'grape_logging/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'grape_logging' 8 | spec.version = GrapeLogging::VERSION 9 | spec.authors = ['aserafin'] 10 | spec.email = ['adrian@softmad.pl'] 11 | 12 | spec.summary = %q{Out of the box request logging for Grape!} 13 | spec.description = %q{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!} 14 | spec.homepage = 'http://github.com/aserafin/grape_logging' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'grape' 23 | spec.add_dependency 'rack' 24 | 25 | spec.add_development_dependency 'bundler', '~> 1.8' 26 | spec.add_development_dependency 'rake', '~> 10.0' 27 | spec.add_development_dependency 'rspec', '~> 3.5' 28 | spec.add_development_dependency 'pry-byebug', '~> 3.4.2' 29 | end 30 | -------------------------------------------------------------------------------- /lib/grape_logging.rb: -------------------------------------------------------------------------------- 1 | require 'grape_logging/multi_io' 2 | require 'grape_logging/version' 3 | require 'grape_logging/formatters/default' 4 | require 'grape_logging/formatters/json' 5 | require 'grape_logging/formatters/lograge' 6 | require 'grape_logging/formatters/logstash' 7 | require 'grape_logging/formatters/rails' 8 | require 'grape_logging/loggers/base' 9 | require 'grape_logging/loggers/response' 10 | require 'grape_logging/loggers/filter_parameters' 11 | require 'grape_logging/loggers/client_env' 12 | require 'grape_logging/loggers/request_headers' 13 | require 'grape_logging/reporters/active_support_reporter' 14 | require 'grape_logging/reporters/logger_reporter' 15 | require 'grape_logging/timings' 16 | require 'grape_logging/middleware/request_logger' 17 | require 'grape_logging/util/parameter_filter' 18 | -------------------------------------------------------------------------------- /lib/grape_logging/formatters/default.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Formatters 3 | class Default 4 | def call(severity, datetime, _, data) 5 | "[#{datetime}] #{severity} -- #{format(data)}\n" 6 | end 7 | 8 | def format(data) 9 | if data.is_a?(String) 10 | data 11 | elsif data.is_a?(Exception) 12 | format_exception(data) 13 | elsif data.is_a?(Hash) 14 | "#{data.delete(:status)} -- #{format_hash(data.delete(:time))} -- #{data.delete(:method)} #{data.delete(:path)} #{format_hash(data)}" 15 | else 16 | data.inspect 17 | end 18 | end 19 | 20 | private 21 | def format_hash(hash) 22 | hash.keys.sort.map { |key| "#{key}=#{hash[key]}" }.join(' ') 23 | end 24 | 25 | def format_exception(exception) 26 | backtrace_array = (exception.backtrace || []).map { |line| "\t#{line}" } 27 | "#{exception.message}\n#{backtrace_array.join("\n")}" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/grape_logging/formatters/json.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Formatters 3 | class Json 4 | def call(severity, datetime, _, data) 5 | { 6 | date: datetime, 7 | severity: severity, 8 | data: format(data) 9 | }.to_json + "\n" 10 | end 11 | 12 | private 13 | 14 | def format(data) 15 | if data.is_a?(String) || data.is_a?(Hash) 16 | data 17 | elsif data.is_a?(Exception) 18 | format_exception(data) 19 | else 20 | data.inspect 21 | end 22 | end 23 | 24 | def format_exception(exception) 25 | { 26 | exception: { 27 | message: exception.message 28 | } 29 | } 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/grape_logging/formatters/lograge.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Formatters 3 | class Lograge 4 | def call(severity, datetime, _, data) 5 | time = data.delete :time 6 | attributes = { 7 | severity: severity, 8 | duration: time[:total], 9 | db: time[:db], 10 | view: time[:view], 11 | datetime: datetime.iso8601 12 | }.merge(data) 13 | ::Lograge.formatter.call(attributes) + "\n" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape_logging/formatters/logstash.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Formatters 3 | class Logstash 4 | def call(severity, datetime, _, data) 5 | { 6 | :'@timestamp' => datetime.iso8601, 7 | :'@version' => '1', 8 | :severity => severity 9 | }.merge!(format(data)).to_json + "\n" 10 | end 11 | 12 | private 13 | 14 | def format(data) 15 | if data.is_a?(Hash) 16 | data 17 | elsif data.is_a?(String) 18 | { message: data } 19 | elsif data.is_a?(Exception) 20 | format_exception(data) 21 | else 22 | { message: data.inspect } 23 | end 24 | end 25 | 26 | def format_exception(exception) 27 | { 28 | exception: { 29 | message: exception.message 30 | } 31 | } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/grape_logging/formatters/rails.rb: -------------------------------------------------------------------------------- 1 | require 'rack/utils' 2 | 3 | module GrapeLogging 4 | module Formatters 5 | class Rails 6 | 7 | def call(severity, datetime, _, data) 8 | if data.is_a?(String) 9 | "#{severity[0..0]} [#{datetime}] #{severity} -- : #{data}\n" 10 | elsif data.is_a?(Exception) 11 | "#{severity[0..0]} [#{datetime}] #{severity} -- : #{format_exception(data)}\n" 12 | elsif data.is_a?(Hash) 13 | format_hash(data) 14 | else 15 | "#{data.inspect}\n" 16 | end 17 | end 18 | 19 | private 20 | 21 | def format_exception(exception) 22 | backtrace_array = (exception.backtrace || []).map { |line| "\t#{line}" } 23 | 24 | [ 25 | "#{exception.message} (#{exception.class})", 26 | backtrace_array.join("\n") 27 | ].reject{|line| line == ""}.join("\n") 28 | end 29 | 30 | def format_hash(hash) 31 | # Create Rails' single summary line at the end of every request, formatted like: 32 | # Completed 200 OK in 958ms (Views: 951.1ms | ActiveRecord: 3.8ms) 33 | # See: actionpack/lib/action_controller/log_subscriber.rb 34 | 35 | message = "" 36 | additions = [] 37 | status = hash.delete(:status) 38 | params = hash.delete(:params) 39 | 40 | total_time = hash[:time] && hash[:time][:total] && hash[:time][:total].round(2) 41 | view_time = hash[:time] && hash[:time][:view] && hash[:time][:view].round(2) 42 | db_time = hash[:time] && hash[:time][:db] && hash[:time][:db].round(2) 43 | 44 | additions << "Views: #{view_time}ms" if view_time 45 | additions << "DB: #{db_time}ms" if db_time 46 | 47 | message << " Parameters: #{params.inspect}\n" if params 48 | 49 | message << "Completed #{status} #{::Rack::Utils::HTTP_STATUS_CODES[status]} in #{total_time}ms" 50 | message << " (#{additions.join(" | ".freeze)})" if additions.size > 0 51 | message << "\n" 52 | message << "\n" if defined?(::Rails.env) && ::Rails.env.development? 53 | 54 | message 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/grape_logging/loggers/base.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Loggers 3 | class Base 4 | def parameters(request, response) 5 | {} 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/grape_logging/loggers/client_env.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Loggers 3 | class ClientEnv < GrapeLogging::Loggers::Base 4 | def parameters(request, _) 5 | { ip: request.env["HTTP_X_FORWARDED_FOR"] || request.env["REMOTE_ADDR"], ua: request.env["HTTP_USER_AGENT"] } 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/grape_logging/loggers/filter_parameters.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Loggers 3 | class FilterParameters < GrapeLogging::Loggers::Base 4 | AD_PARAMS = 'action_dispatch.request.parameters'.freeze 5 | 6 | def initialize(filter_parameters = nil, replacement = nil, exceptions = %w(controller action format)) 7 | @filter_parameters = filter_parameters || (defined?(::Rails.application) ? ::Rails.application.config.filter_parameters : []) 8 | @replacement = replacement || '[FILTERED]' 9 | @exceptions = exceptions 10 | end 11 | 12 | def parameters(request, _) 13 | { params: safe_parameters(request) } 14 | end 15 | 16 | private 17 | 18 | def parameter_filter 19 | @parameter_filter ||= ParameterFilter.new(@replacement, @filter_parameters) 20 | end 21 | 22 | def safe_parameters(request) 23 | # Now this logger can work also over Rails requests 24 | if request.params.empty? 25 | clean_parameters(request.env[AD_PARAMS] || {}) 26 | else 27 | clean_parameters(request.params) 28 | end 29 | end 30 | 31 | def clean_parameters(parameters) 32 | parameter_filter.filter(parameters).reject{ |key, _value| @exceptions.include?(key) } 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/grape_logging/loggers/request_headers.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Loggers 3 | class RequestHeaders < GrapeLogging::Loggers::Base 4 | 5 | HTTP_PREFIX = 'HTTP_'.freeze 6 | 7 | def parameters(request, _) 8 | headers = {} 9 | 10 | request.env.each_pair do |k, v| 11 | next unless k.to_s.start_with? HTTP_PREFIX 12 | 13 | k = k[5..-1].split('_').each(&:capitalize!).join('-') 14 | headers[k] = v 15 | end 16 | 17 | { headers: headers } 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/grape_logging/loggers/response.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Loggers 3 | class Response < GrapeLogging::Loggers::Base 4 | def parameters(_, response) 5 | response ? { response: serialized_response_body(response) } : {} 6 | end 7 | 8 | private 9 | 10 | # In some cases, response.body is not parseable by JSON. 11 | # For example, if you POST on a PUT endpoint, response.body is egal to """". 12 | # It's strange but it's the Grape behavior... 13 | def serialized_response_body(response) 14 | 15 | if response.respond_to?(:body) 16 | # Rack responses 17 | begin 18 | response.body.map{ |body| JSON.parse(body.to_s) } 19 | rescue # No reason to have "=> e" here when we don't use it.. 20 | response.body 21 | end 22 | else 23 | # Error & Exception responses 24 | response 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/grape_logging/middleware/request_logger.rb: -------------------------------------------------------------------------------- 1 | require 'grape/middleware/base' 2 | 3 | module GrapeLogging 4 | module Middleware 5 | class RequestLogger < Grape::Middleware::Base 6 | 7 | ActiveSupport::Notifications.subscribe('sql.active_record') do |*args| 8 | event = ActiveSupport::Notifications::Event.new(*args) 9 | GrapeLogging::Timings.append_db_runtime(event) 10 | end if defined?(ActiveRecord) 11 | 12 | # Persist response status & response (body) 13 | # to use int in parameters 14 | attr_accessor :response_status, :response_body 15 | 16 | def initialize(app, options = {}) 17 | super 18 | 19 | @included_loggers = @options[:include] || [] 20 | @reporter = if options[:instrumentation_key] 21 | Reporters::ActiveSupportReporter.new(@options[:instrumentation_key]) 22 | else 23 | Reporters::LoggerReporter.new(@options[:logger], @options[:formatter], @options[:log_level]) 24 | end 25 | end 26 | 27 | def before 28 | reset_db_runtime 29 | start_time 30 | invoke_included_loggers(:before) 31 | end 32 | 33 | def after(status, response) 34 | stop_time 35 | 36 | # Response status 37 | @response_status = status 38 | @response_body = response 39 | 40 | # Perform repotters 41 | @reporter.perform(collect_parameters) 42 | 43 | # Invoke loggers 44 | invoke_included_loggers(:after) 45 | nil 46 | end 47 | 48 | # Call stack and parse responses & status. 49 | # 50 | # @note Exceptions are logged as 500 status & re-raised. 51 | def call!(env) 52 | @env = env 53 | 54 | # Before hook 55 | before 56 | 57 | # Catch error 58 | error = catch(:error) do 59 | begin 60 | @app_response = @app.call(@env) 61 | rescue => e 62 | # Log as 500 + message 63 | after(e.respond_to?(:status) ? e.status : 500, e.message) 64 | 65 | # Re-raise exception 66 | raise e 67 | end 68 | nil 69 | end 70 | 71 | # Get status & response from app_response 72 | # when no error occures. 73 | if error 74 | # Call with error & response 75 | after(error[:status], error[:message]) 76 | 77 | # Throw again 78 | throw(:error, error) 79 | else 80 | status, _, resp = *@app_response 81 | 82 | # Call after hook properly 83 | after(status, resp) 84 | end 85 | 86 | # Otherwise return original response 87 | @app_response 88 | end 89 | 90 | protected 91 | 92 | def parameters 93 | { 94 | status: response_status, 95 | time: { 96 | total: total_runtime, 97 | db: db_runtime, 98 | view: view_runtime 99 | }, 100 | method: request.request_method, 101 | path: request.path, 102 | params: request.params, 103 | host: request.host 104 | } 105 | end 106 | 107 | private 108 | 109 | def request 110 | @request ||= ::Rack::Request.new(@env) 111 | end 112 | 113 | def total_runtime 114 | ((stop_time - start_time) * 1000).round(2) 115 | end 116 | 117 | def view_runtime 118 | total_runtime - db_runtime 119 | end 120 | 121 | def db_runtime 122 | GrapeLogging::Timings.db_runtime.round(2) 123 | end 124 | 125 | def reset_db_runtime 126 | GrapeLogging::Timings.reset_db_runtime 127 | end 128 | 129 | def start_time 130 | @start_time ||= Time.now 131 | end 132 | 133 | def stop_time 134 | @stop_time ||= Time.now 135 | end 136 | 137 | def collect_parameters 138 | parameters.tap do |params| 139 | @included_loggers.each do |logger| 140 | params.merge! logger.parameters(request, response_body) do |_, oldval, newval| 141 | oldval.respond_to?(:merge) ? oldval.merge(newval) : newval 142 | end 143 | end 144 | end 145 | end 146 | 147 | def invoke_included_loggers(method_name) 148 | @included_loggers.each do |logger| 149 | logger.send(method_name) if logger.respond_to?(method_name) 150 | end 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/grape_logging/multi_io.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | class MultiIO 3 | def initialize(*targets) 4 | @targets = targets 5 | end 6 | 7 | def write(*args) 8 | @targets.each {|t| t.write(*args)} 9 | end 10 | 11 | def close 12 | @targets.each(&:close) 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/grape_logging/reporters/active_support_reporter.rb: -------------------------------------------------------------------------------- 1 | module Reporters 2 | class ActiveSupportReporter 3 | def initialize(instrumentation_key) 4 | @instrumentation_key = instrumentation_key 5 | end 6 | 7 | def perform(params) 8 | ActiveSupport::Notifications.instrument @instrumentation_key, params 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/grape_logging/reporters/logger_reporter.rb: -------------------------------------------------------------------------------- 1 | module Reporters 2 | class LoggerReporter 3 | def initialize(logger, formatter, log_level) 4 | @logger = logger || Logger.new(STDOUT) 5 | @log_level = log_level || :info 6 | if @logger.respond_to?(:formatter=) 7 | @logger.formatter = formatter || @logger.formatter || GrapeLogging::Formatters::Default.new 8 | end 9 | end 10 | 11 | def perform(params) 12 | @logger.send(@log_level, params) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape_logging/timings.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | module Timings 3 | extend self 4 | 5 | def db_runtime=(value) 6 | Thread.current[:grape_db_runtime] = value 7 | end 8 | 9 | def db_runtime 10 | Thread.current[:grape_db_runtime] ||= 0 11 | end 12 | 13 | def reset_db_runtime 14 | self.db_runtime = 0 15 | end 16 | 17 | def append_db_runtime(event) 18 | self.db_runtime += event.duration 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/grape_logging/util/parameter_filter.rb: -------------------------------------------------------------------------------- 1 | if defined?(::Rails.application) 2 | if Gem::Version.new(Rails.version) < Gem::Version.new('6.0.0') 3 | class ParameterFilter < ActionDispatch::Http::ParameterFilter 4 | def initialize(_replacement, filter_parameters) 5 | super(filter_parameters) 6 | end 7 | end 8 | else 9 | require "active_support/parameter_filter" 10 | 11 | class ParameterFilter < ActiveSupport::ParameterFilter 12 | def initialize(_replacement, filter_parameters) 13 | super(filter_parameters) 14 | end 15 | end 16 | end 17 | else 18 | # 19 | # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameter_filter.rb 20 | # we could depend on Rails specifically, but that would us way to hefty! 21 | # 22 | class ParameterFilter 23 | def initialize(replacement, filters = []) 24 | @replacement = replacement 25 | @filters = filters 26 | end 27 | 28 | def filter(params) 29 | compiled_filter.call(params) 30 | end 31 | 32 | private 33 | 34 | def compiled_filter 35 | @compiled_filter ||= CompiledFilter.compile(@replacement, @filters) 36 | end 37 | 38 | class CompiledFilter # :nodoc: 39 | def self.compile(replacement, filters) 40 | return lambda { |params| params.dup } if filters.empty? 41 | 42 | strings, regexps, blocks = [], [], [] 43 | 44 | filters.each do |item| 45 | case item 46 | when Proc 47 | blocks << item 48 | when Regexp 49 | regexps << item 50 | else 51 | strings << Regexp.escape(item.to_s) 52 | end 53 | end 54 | 55 | deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } 56 | deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } 57 | 58 | regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? 59 | deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? 60 | 61 | new replacement, regexps, deep_regexps, blocks 62 | end 63 | 64 | attr_reader :regexps, :deep_regexps, :blocks 65 | 66 | def initialize(replacement, regexps, deep_regexps, blocks) 67 | @replacement = replacement 68 | @regexps = regexps 69 | @deep_regexps = deep_regexps.any? ? deep_regexps : nil 70 | @blocks = blocks 71 | end 72 | 73 | def call(original_params, parents = []) 74 | filtered_params = {} 75 | 76 | original_params.each do |key, value| 77 | parents.push(key) if deep_regexps 78 | if regexps.any? { |r| key =~ r } 79 | value = @replacement 80 | elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } 81 | value = @replacement 82 | elsif value.is_a?(Hash) 83 | value = call(value, parents) 84 | elsif value.is_a?(Array) 85 | value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } 86 | elsif blocks.any? 87 | key = key.dup if key.duplicable? 88 | value = value.dup if value.duplicable? 89 | blocks.each { |b| b.call(key, value) } 90 | end 91 | parents.pop if deep_regexps 92 | 93 | filtered_params[key] = value 94 | end 95 | 96 | filtered_params 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/grape_logging/version.rb: -------------------------------------------------------------------------------- 1 | module GrapeLogging 2 | VERSION = '1.8.4' 3 | end 4 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/formatters/rails_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GrapeLogging::Formatters::Rails do 4 | let(:formatter) { described_class.new } 5 | let(:severity) { "INFO" } 6 | let(:datetime) { Time.new('2018', '03', '02', '10', '35', '04', '+13:00') } 7 | 8 | let(:exception_data) { ArgumentError.new('Message') } 9 | let(:hash_data) { 10 | { 11 | status: 200, 12 | time: { 13 | total: 272.4, 14 | db: 40.63, 15 | view: 231.76999999999998 16 | }, 17 | method: "GET", 18 | path: "/api/endpoint", 19 | host: "localhost" 20 | } 21 | } 22 | 23 | describe '#call' do 24 | context 'string data' do 25 | it 'returns a formatted string' do 26 | message = formatter.call(severity, datetime, nil, 'value') 27 | 28 | expect(message).to eq "I [2018-03-02 10:35:04 +1300] INFO -- : value\n" 29 | end 30 | end 31 | 32 | context 'exception data' do 33 | it 'returns a string with a backtrace' do 34 | exception_data.set_backtrace(caller) 35 | 36 | message = formatter.call(severity, datetime, nil, exception_data) 37 | lines = message.split("\n") 38 | 39 | expect(lines[0]).to eq "I [2018-03-02 10:35:04 +1300] INFO -- : Message (ArgumentError)" 40 | expect(lines[1]).to include 'grape_logging' 41 | expect(lines.size).to be > 1 42 | end 43 | end 44 | 45 | context 'hash data' do 46 | it 'returns a formatted string' do 47 | message = formatter.call(severity, datetime, nil, hash_data) 48 | 49 | expect(message).to eq "Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)\n" 50 | end 51 | 52 | it 'includes params if included (from GrapeLogging::Loggers::FilterParameters)' do 53 | hash_data.merge!( 54 | params: { 55 | "some_param" => { 56 | value_1: "123", 57 | value_2: "456" 58 | } 59 | } 60 | ) 61 | 62 | message = formatter.call(severity, datetime, nil, hash_data) 63 | lines = message.split("\n") 64 | 65 | expect(lines.first).to eq ' Parameters: {"some_param"=>{:value_1=>"123", :value_2=>"456"}}' 66 | expect(lines.last).to eq "Completed 200 OK in 272.4ms (Views: 231.77ms | DB: 40.63ms)" 67 | end 68 | end 69 | 70 | context "unhandled data" do 71 | it 'returns the #inspect string representation' do 72 | message = formatter.call(severity, datetime, nil, [1, 2, 3]) 73 | 74 | expect(message).to eq "[1, 2, 3]\n" 75 | end 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/loggers/client_env_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | describe GrapeLogging::Loggers::ClientEnv do 5 | let(:ip) { '10.0.0.1' } 6 | let(:user_agent) { 'user agent' } 7 | let(:forwarded_for) { "forwarded for" } 8 | let(:remote_addr) { "remote address" } 9 | 10 | context 'forwarded for' do 11 | let(:mock_request) do 12 | OpenStruct.new(env: { 13 | "HTTP_X_FORWARDED_FOR" => forwarded_for 14 | }) 15 | end 16 | 17 | it 'sets the ip key' do 18 | expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil) 19 | end 20 | 21 | it 'prefers the forwarded_for over the remote_addr' do 22 | mock_request.env['REMOTE_ADDR'] = remote_addr 23 | expect(subject.parameters(mock_request, nil)).to eq(ip: forwarded_for, ua: nil) 24 | end 25 | end 26 | 27 | context 'remote address' do 28 | let(:mock_request) do 29 | OpenStruct.new(env: { 30 | "REMOTE_ADDR" => remote_addr 31 | }) 32 | end 33 | 34 | it 'sets the ip key' do 35 | expect(subject.parameters(mock_request, nil)).to eq(ip: remote_addr, ua: nil) 36 | end 37 | end 38 | 39 | context 'user agent' do 40 | let(:mock_request) do 41 | OpenStruct.new(env: { 42 | "HTTP_USER_AGENT" => user_agent 43 | }) 44 | end 45 | 46 | it 'sets the ua key' do 47 | expect(subject.parameters(mock_request, nil)).to eq(ip: nil, ua: user_agent) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/loggers/filter_parameters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | 5 | describe GrapeLogging::Loggers::FilterParameters do 6 | let(:filtered_parameters) { %w[one four] } 7 | 8 | let(:mock_request) do 9 | OpenStruct.new(params: { 10 | this_one: 'this one', 11 | that_one: 'one', 12 | two: 'two', 13 | three: 'three', 14 | four: 'four' 15 | }) 16 | end 17 | 18 | let(:mock_request_with_deep_nesting) do 19 | deep_clone = lambda { Marshal.load Marshal.dump mock_request.params } 20 | OpenStruct.new( 21 | params: deep_clone.call.merge( 22 | five: deep_clone.call.merge( 23 | deep_clone.call.merge({six: {seven: 'seven', eight: 'eight', one: 'another one'}}) 24 | ) 25 | ) 26 | ) 27 | end 28 | 29 | let(:subject) do 30 | GrapeLogging::Loggers::FilterParameters.new filtered_parameters, replacement 31 | end 32 | 33 | let(:replacement) { nil } 34 | 35 | shared_examples 'filtering' do 36 | it 'filters out sensitive parameters' do 37 | expect(subject.parameters(mock_request, nil)).to eq(params: { 38 | this_one: subject.instance_variable_get('@replacement'), 39 | that_one: subject.instance_variable_get('@replacement'), 40 | two: 'two', 41 | three: 'three', 42 | four: subject.instance_variable_get('@replacement'), 43 | }) 44 | end 45 | 46 | it 'deeply filters out sensitive parameters' do 47 | expect(subject.parameters(mock_request_with_deep_nesting, nil)).to eq(params: { 48 | this_one: subject.instance_variable_get('@replacement'), 49 | that_one: subject.instance_variable_get('@replacement'), 50 | two: 'two', 51 | three: 'three', 52 | four: subject.instance_variable_get('@replacement'), 53 | five: { 54 | this_one: subject.instance_variable_get('@replacement'), 55 | that_one: subject.instance_variable_get('@replacement'), 56 | two: 'two', 57 | three: 'three', 58 | four: subject.instance_variable_get('@replacement'), 59 | six: { 60 | seven: 'seven', 61 | eight: 'eight', 62 | one: subject.instance_variable_get('@replacement'), 63 | }, 64 | }, 65 | }) 66 | end 67 | end 68 | 69 | context 'with default replacement' do 70 | it_behaves_like 'filtering' 71 | end 72 | 73 | context 'with custom replacement' do 74 | let(:replacement) { 'CUSTOM_REPLACEMENT' } 75 | it_behaves_like 'filtering' 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/loggers/request_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | describe GrapeLogging::Loggers::RequestHeaders do 5 | let(:mock_request) do 6 | OpenStruct.new(env: {HTTP_REFERER: 'http://example.com', HTTP_ACCEPT: 'text/plain'}) 7 | end 8 | 9 | let(:mock_request_with_unhandle_headers) do 10 | OpenStruct.new(env: { 11 | HTTP_REFERER: 'http://example.com', 12 | "PATH_INFO"=>"/api/v1/users" 13 | }) 14 | end 15 | 16 | let(:mock_request_with_long_headers) do 17 | OpenStruct.new(env: { 18 | HTTP_REFERER: 'http://example.com', 19 | HTTP_USER_AGENT: "Mozilla/5.0" 20 | }) 21 | end 22 | 23 | it 'strips HTTP_ from the parameter' do 24 | expect(subject.parameters(mock_request, nil)).to eq({ 25 | headers: {'Referer' => 'http://example.com', 'Accept' => 'text/plain'} 26 | }) 27 | end 28 | 29 | it 'only handle things which start with HTTP_' do 30 | expect(subject.parameters(mock_request_with_unhandle_headers, nil)).to eq({ 31 | headers: {'Referer' => 'http://example.com' } 32 | }) 33 | end 34 | 35 | it 'substitutes _ with -' do 36 | expect(subject.parameters(mock_request_with_long_headers, nil)).to eq({ 37 | headers: {'Referer' => 'http://example.com', 'User-Agent' => 'Mozilla/5.0' } 38 | }) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/loggers/response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | describe GrapeLogging::Loggers::Response do 5 | context 'with a parseable JSON body' do 6 | let(:response) do 7 | OpenStruct.new(body: [%q{{"one": "two", "three": {"four": 5}}}]) 8 | end 9 | 10 | it 'returns an array of parseable JSON objects' do 11 | expect(subject.parameters(nil, response)).to eq({ 12 | response: [response.body.first.dup] 13 | }) 14 | end 15 | end 16 | 17 | context 'with a body that is not parseable JSON' do 18 | let(:response) do 19 | OpenStruct.new(body: "this is a body") 20 | end 21 | 22 | it 'just returns the body' do 23 | expect(subject.parameters(nil, response)).to eq({ 24 | response: response.body.dup 25 | }) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/grape_logging/middleware/request_logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rack' 3 | 4 | describe GrapeLogging::Middleware::RequestLogger do 5 | let(:subject) { request.send(request_method, path) } 6 | let(:app) { proc{ [status, {} , ['response body']] } } 7 | let(:stack) { described_class.new app, options } 8 | let(:request) { Rack::MockRequest.new(stack) } 9 | let(:options) { {include: [], logger: logger} } 10 | let(:logger) { double('logger') } 11 | let(:path) { '/' } 12 | let(:request_method) { 'get' } 13 | let(:status) { 200 } 14 | 15 | it 'logs to the logger' do 16 | expect(logger).to receive('info') do |arguments| 17 | expect(arguments[:status]).to eq 200 18 | expect(arguments[:method]).to eq 'GET' 19 | expect(arguments[:params]).to be_empty 20 | expect(arguments[:host]).to eq 'example.org' 21 | expect(arguments).to have_key :time 22 | expect(arguments[:time]).to have_key :total 23 | expect(arguments[:time]).to have_key :db 24 | expect(arguments[:time]).to have_key :view 25 | end 26 | subject 27 | end 28 | 29 | [301, 404, 500].each do |the_status| 30 | context "when the respnse status is #{the_status}" do 31 | let(:status) { the_status } 32 | it 'should log the correct status code' do 33 | expect(logger).to receive('info') do |arguments| 34 | expect(arguments[:status]).to eq the_status 35 | end 36 | subject 37 | end 38 | end 39 | end 40 | 41 | %w[info error debug].each do |level| 42 | context "with level #{level}" do 43 | it 'should log at correct level' do 44 | options[:log_level] = level 45 | expect(logger).to receive(level) 46 | subject 47 | end 48 | end 49 | end 50 | 51 | context 'with a nil response' do 52 | let(:app) { proc{ [500, {} , nil] } } 53 | it 'should log "fail" instead of a status' do 54 | expect(Rack::MockResponse).to receive(:new) { nil } 55 | expect(logger).to receive('info') do |arguments| 56 | expect(arguments[:status]).to eq 500 57 | end 58 | subject 59 | end 60 | end 61 | 62 | context 'additional_loggers' do 63 | before do 64 | options[:include] << GrapeLogging::Loggers::RequestHeaders.new 65 | options[:include] << GrapeLogging::Loggers::ClientEnv.new 66 | options[:include] << GrapeLogging::Loggers::Response.new 67 | options[:include] << GrapeLogging::Loggers::FilterParameters.new(["replace_me"]) 68 | end 69 | 70 | %w[get put post delete options head patch].each do |the_method| 71 | let(:request_method) { the_method } 72 | context "with HTTP method[#{the_method}]" do 73 | it 'should include additional information in the log' do 74 | expect(logger).to receive('info') do |arguments| 75 | expect(arguments).to have_key :headers 76 | expect(arguments).to have_key :ip 77 | expect(arguments).to have_key :response 78 | end 79 | subject 80 | end 81 | end 82 | end 83 | 84 | it 'should filter parameters in the log' do 85 | expect(logger).to receive('info') do |arguments| 86 | expect(arguments[:params]).to eq( 87 | "replace_me" => '[FILTERED]', 88 | "replace_me_too" => '[FILTERED]', 89 | "cant_touch_this" => 'should see' 90 | ) 91 | end 92 | parameters = { 93 | 'replace_me' => 'should not see', 94 | 'replace_me_too' => 'should not see', 95 | 'cant_touch_this' => 'should see' 96 | } 97 | request.post path, params: parameters 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift '.' 2 | 3 | require 'lib/grape_logging' 4 | 5 | RSpec.configure do |config| 6 | # rspec-expectations config goes here. You can use an alternate 7 | # assertion/expectation library such as wrong or the stdlib/minitest 8 | # assertions if you prefer. 9 | config.expect_with :rspec do |expectations| 10 | # This option will default to `true` in RSpec 4. It makes the `description` 11 | # and `failure_message` of custom matchers include text for helper methods 12 | # defined using `chain`, e.g.: 13 | # be_bigger_than(2).and_smaller_than(4).description 14 | # # => "be bigger than 2 and smaller than 4" 15 | # ...rather than: 16 | # # => "be bigger than 2" 17 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 18 | end 19 | 20 | # rspec-mocks config goes here. You can use an alternate test double 21 | # library (such as bogus or mocha) by changing the `mock_with` option here. 22 | config.mock_with :rspec do |mocks| 23 | # Prevents you from mocking or stubbing a method that does not exist on 24 | # a real object. This is generally recommended, and will default to 25 | # `true` in RSpec 4. 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 30 | # have no way to turn it off -- the option exists only for backwards 31 | # compatibility in RSpec 3). It causes shared context metadata to be 32 | # inherited by the metadata hash of host groups and examples, rather than 33 | # triggering implicit auto-inclusion in groups with matching metadata. 34 | config.shared_context_metadata_behavior = :apply_to_host_groups 35 | 36 | # The settings below are suggested to provide a good initial experience 37 | # with RSpec, but feel free to customize to your heart's content. 38 | =begin 39 | # This allows you to limit a spec run to individual examples or groups 40 | # you care about by tagging them with `:focus` metadata. When nothing 41 | # is tagged with `:focus`, all examples get run. RSpec also provides 42 | # aliases for `it`, `describe`, and `context` that include `:focus` 43 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 44 | config.filter_run_when_matching :focus 45 | 46 | # Allows RSpec to persist some state between runs in order to support 47 | # the `--only-failures` and `--next-failure` CLI options. We recommend 48 | # you configure your source control system to ignore this file. 49 | config.example_status_persistence_file_path = "spec/examples.txt" 50 | 51 | # Limits the available syntax to the non-monkey patched syntax that is 52 | # recommended. For more details, see: 53 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 54 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 55 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 56 | config.disable_monkey_patching! 57 | 58 | # This setting enables warnings. It's recommended, but in some cases may 59 | # be too noisy due to issues in dependencies. 60 | config.warnings = true 61 | 62 | # Many RSpec users commonly either run the entire suite or an individual 63 | # file, and it's useful to allow more verbose output when running an 64 | # individual spec file. 65 | if config.files_to_run.one? 66 | # Use the documentation formatter for detailed output, 67 | # unless a formatter has already been configured 68 | # (e.g. via a command-line flag). 69 | config.default_formatter = 'doc' 70 | end 71 | 72 | # Print the 10 slowest examples and example groups at the 73 | # end of the spec run, to help surface which specs are running 74 | # particularly slow. 75 | config.profile_examples = 10 76 | 77 | # Run specs in random order to surface order dependencies. If you find an 78 | # order dependency and want to debug it, you can fix the order by providing 79 | # the seed, which is printed after each run. 80 | # --seed 1234 81 | config.order = :random 82 | 83 | # Seed global randomization in this process using the `--seed` CLI option. 84 | # Setting this allows you to use `--seed` to deterministically reproduce 85 | # test failures related to randomization by passing the same `--seed` value 86 | # as the one that triggered the failure. 87 | Kernel.srand config.seed 88 | =end 89 | end 90 | --------------------------------------------------------------------------------