├── .github ├── dependabot.yml └── workflows │ ├── release-please.yml │ └── tests.yml ├── .gitignore ├── .release-please-manifest.json ├── .rspec ├── .rubocop.yml ├── .tool-versions ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── doc └── dependency_decisions.yml ├── faraday-detailed_logger.gemspec ├── gemfiles ├── faraday_0.16.x.gemfile ├── faraday_0.x.gemfile ├── faraday_1.x.gemfile ├── faraday_2.x.gemfile ├── faraday_canary.gemfile └── faraday_latest.gemfile ├── lib └── faraday │ ├── detailed_logger.rb │ └── detailed_logger │ ├── curl_formatter.rb │ ├── middleware.rb │ ├── middleware │ ├── current.rb │ └── legacy.rb │ ├── tagged_logging.rb │ └── version.rb ├── release-please-config.json └── spec ├── faraday └── detailed_logger │ ├── curl_formatter_spec.rb │ └── middleware_spec.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | updates: 3 | - allow: 4 | - dependency-type: "direct" 5 | - dependency-type: "indirect" 6 | directory: "/" 7 | open-pull-requests-limit: 10 8 | package-ecosystem: "bundler" 9 | schedule: 10 | interval: "daily" 11 | - directory: "/" 12 | package-ecosystem: "github-actions" 13 | schedule: 14 | interval: "daily" 15 | version: 2 16 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | release-please: 4 | outputs: 5 | release_created: "${{ steps.release.outputs.release_created }}" 6 | tag_name: "${{ steps.release.outputs.tag_name }}" 7 | version_number: "${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}" 8 | permissions: 9 | contents: "write" 10 | issues: "write" 11 | pull-requests: "write" 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - id: "release" 15 | uses: "googleapis/release-please-action@v4" 16 | 17 | release: 18 | if: "${{ needs.release-please.outputs.release_created }}" 19 | name: "Release" 20 | needs: 21 | - "release-please" 22 | permissions: 23 | id-token: "write" 24 | contents: "write" 25 | runs-on: "ubuntu-latest" 26 | steps: 27 | - uses: "actions/checkout@v4" 28 | with: 29 | fetch-tags: true 30 | - uses: "ruby/setup-ruby@v1" 31 | with: 32 | bundler-cache: true 33 | - uses: "rubygems/release-gem@v1" 34 | 35 | name: "Release" 36 | 37 | "on": 38 | push: 39 | branches: 40 | - "main" 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | tests: 4 | env: 5 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 6 | name: "Tests" 7 | runs-on: "ubuntu-latest" 8 | steps: 9 | - uses: "actions/checkout@v4" 10 | - uses: "ruby/setup-ruby@v1" 11 | with: 12 | bundler-cache: true 13 | - name: "Run Tests" 14 | run: "bundle exec rake spec" 15 | - name: "Check Ruby Style" 16 | run: "bundle exec rubocop -c ./.rubocop.yml -fq" 17 | - name: "Check Licenses" 18 | run: | 19 | # Forcibly set Bundler platform to patch for https://github.com/pivotal/LicenseFinder/issues/828 20 | bundle config --local deployment false 21 | bundle lock --add-platform x86_64-linux 22 | bundle config --local deployment true 23 | 24 | bundle exec license_finder --quiet 25 | strategy: 26 | matrix: 27 | gemfile: 28 | - "gemfiles/faraday_latest.gemfile" 29 | - "gemfiles/faraday_0.16.x.gemfile" 30 | - "gemfiles/faraday_0.x.gemfile" 31 | - "gemfiles/faraday_1.x.gemfile" 32 | - "gemfiles/faraday_2.x.gemfile" 33 | name: "Tests" 34 | "on": 35 | pull_request: 36 | branches: 37 | - "main" 38 | push: 39 | branches: 40 | - "main" 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | /gemfiles/.bundle/ 14 | /gemfiles/*.gemfile.lock 15 | /Gemfile.lock 16 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.6.1" 3 | } -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --no-profile 3 | --require spec_helper 4 | --warnings 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | inherit_gem: 4 | rubocop-rails_config: 5 | - "config/rails.yml" 6 | 7 | AllCops: 8 | Exclude: 9 | - 'gemfiles/*' 10 | - 'gemfiles/**/*' 11 | - 'vendor/**/*' 12 | TargetRubyVersion: "2.7" 13 | 14 | Layout/CaseIndentation: 15 | EnforcedStyle: "end" 16 | 17 | Layout/EmptyLinesAroundAccessModifier: 18 | EnforcedStyle: "around" 19 | 20 | Layout/EndOfLine: 21 | EnforcedStyle: "lf" 22 | 23 | Layout/IndentationConsistency: 24 | EnforcedStyle: "normal" 25 | 26 | Style/StringLiterals: 27 | EnforcedStyle: "single_quotes" 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4.2 2 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'faraday-0.16.x' do 4 | gem 'faraday', '~> 0.16.0' 5 | end 6 | 7 | appraise 'faraday-0.x' do 8 | gem 'faraday', '~> 0.16' 9 | end 10 | 11 | appraise 'faraday-1.x' do 12 | gem 'faraday', '~> 1.0' 13 | end 14 | 15 | appraise 'faraday-2.x' do 16 | gem 'faraday', '~> 2.0' 17 | end 18 | 19 | appraise 'faraday-latest' do 20 | gem 'faraday' 21 | end 22 | 23 | appraise 'faraday-canary' do 24 | gem 'faraday', git: 'https://github.com/lostisland/faraday.git' 25 | end 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Faraday::DetailedLogger changelog 2 | 3 | [![Gem Version](https://badge.fury.io/rb/faraday-detailed_logger.svg)](https://badge.fury.io/rb/faraday-detailed_logger) 4 | [![Tests](https://github.com/envylabs/faraday-detailed_logger/actions/workflows/tests.yml/badge.svg)](https://github.com/envylabs/faraday-detailed_logger/actions/workflows/tests.yml) 5 | 6 | ## [2.6.1](https://github.com/envylabs/faraday-detailed_logger/compare/v2.6.0...v2.6.1) (2025-04-20) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * re-release of v2.6.0 after release automation corrections ([0deca63](https://github.com/envylabs/faraday-detailed_logger/commit/0deca63ec5dd6dbb2fd4a64d2ef74a0e67df8d33)) 12 | 13 | ## [2.6.0](https://github.com/envylabs/faraday-detailed_logger/compare/v2.5.0...v2.6.0) (2025-04-20) 14 | 15 | 16 | ### Features 17 | 18 | * set minimum Ruby to version 3.2 ([d6e5bd4](https://github.com/envylabs/faraday-detailed_logger/commit/d6e5bd4bf2e375370e03b39f4e8e0f053476517b)) 19 | 20 | ## [HEAD][] / unreleased 21 | 22 | * No significant changes. 23 | 24 | ## [2.5.0][] / 2022-01-23 25 | 26 | * Add faraday 2 support. 27 | * Extract an initial internal cURL formatter 28 | 29 | ## [2.4.2][] / 2021-11-09 30 | 31 | * Revert zeitwerk autoloading changes. 32 | 33 | The existing implementation caused application errors citing a `uninitialized 34 | constant Faraday::DetailedLogger::Middleware`. 35 | 36 | ## [2.4.1][] / 2021-11-09 37 | 38 | * Fix `Faraday::Error: :detailed_logger is not registered on Faraday::Response` 39 | by relocating middleware registration. 40 | * Increase the low-end of the faraday dependency to `~> 0.16` for Ruby 3 41 | compatibility. Versions of faraday earlier than 0.16 are incompatible with 42 | Ruby 3's Proc interface. 43 | 44 | ## [2.4.0][] / 2021-11-06 45 | 46 | * Use [zeitwerk](https://rubygems.org/gems/zeitwerk) autoloading. 47 | 48 | ## [2.3.0][] / 2020-02-11 49 | 50 | * Sort the request and response headers when logging. 51 | 52 | ## [2.2.0][] / 2020-01-02 53 | 54 | * Add support for faraday 1.0. 55 | 56 | ## [2.1.3][] / 2019-04-04 57 | 58 | * Maintenance release, no functional changes. 59 | 60 | ## [2.1.2][] / 2017-08-21 61 | 62 | * Update the middleware to allow the `logger` and `tags` to be publicly 63 | accessible. This is not necessarily intended to be developer-used, but rather 64 | fix warnings in older versions of Ruby. 65 | 66 | ## [2.1.1][] / 2017-01-07 67 | 68 | * Require faraday `~> 0.8`. This change only makes explicit the minimum version 69 | of faraday which is supported. 70 | 71 | ## [2.1.0][] / 2016-10-12 72 | 73 | * Catch StandardError exceptions to log and re-raise them. 74 | 75 | ## [2.0.0][] / 2016-07-08 76 | 77 | * Remove Logger `progname` support/configuration. Varying the progname appears 78 | to make logging in a syslog-like environment unnecessarily more difficult. 79 | * Add tagging support to the logger. Any number of tags may be given which will 80 | be prepended to all lines logged. This is largely follows the 81 | ActiveSupport::TaggedLogging log functionality. "Old" usages of this library 82 | will treat any previous `progname` strings as a tag and continue to record 83 | them to the log. Even though the progname is now logged as a tag, this is 84 | still considered a breaking change just in case system configurations were 85 | dependent on the progname for log output redirection (syslog). 86 | 87 | ## [1.1.0][] / 2016-06-15 88 | 89 | * Log HTTP 4XX and HTTP 5XX responses at a WARN level. 90 | 91 | ## [1.0.0][] / 2014-07-02 92 | 93 | * Stable release. 94 | 95 | [1.0.0]: https://github.com/envylabs/faraday-detailed_logger/tree/v1.0.0 96 | [1.1.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v1.0.0...v1.1.0 97 | [2.0.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v1.1.0...v2.0.0 98 | [2.1.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.0.0...v2.1.0 99 | [2.1.1]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.1.0...v2.1.1 100 | [2.1.2]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.1.1...v2.1.2 101 | [2.1.3]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.1.2...v2.1.3 102 | [2.2.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.1.3...v2.2.0 103 | [2.3.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.2.0...v2.3.0 104 | [2.4.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.3.0...v2.4.0 105 | [2.4.1]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.4.0...v2.4.1 106 | [2.4.2]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.4.1...v2.4.2 107 | [2.5.0]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.4.2...v2.5.0 108 | [HEAD]: https://github.com/envylabs/faraday-detailed_logger/compare/v2.5.0...main 109 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Envy Labs, LLC 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 | # Faraday::DetailedLogger 2 | 3 | [![Gem Version](https://badge.fury.io/rb/faraday-detailed_logger.svg)](https://badge.fury.io/rb/faraday-detailed_logger) 4 | [![Tests](https://github.com/envylabs/faraday-detailed_logger/actions/workflows/tests.yml/badge.svg)](https://github.com/envylabs/faraday-detailed_logger/actions/workflows/tests.yml) 5 | 6 | A Faraday middleware used for providing debug- and info-level logging 7 | information. The request and response logs follow very closely with cURL output 8 | for ease of understanding. 9 | 10 | **Caution:** Be careful about your log level settings when using this 11 | middleware, _especially_ in a production environment. With a DEBUG level log 12 | enabled, there will be information security concerns. 13 | 14 | At a DEBUG level, the request and response headers and their bodies will be 15 | logged. This means that if you have Authorization information or API keys in 16 | your headers or are passing around sensitive information in the bodies, only an 17 | INFO level or above should be used. 18 | 19 | No headers or bodies are logged at an INFO or greater log level. 20 | 21 | ## Installation 22 | 23 | Add this line to your application's Gemfile: 24 | 25 | ```ruby 26 | gem 'faraday-detailed_logger' 27 | ``` 28 | 29 | And then execute: 30 | 31 | ```bash 32 | $ bundle 33 | ``` 34 | 35 | Or install it yourself as: 36 | 37 | ```bash 38 | $ gem install faraday-detailed_logger 39 | ``` 40 | 41 | ## Usage 42 | 43 | Once required, the logger can be added to any Faraday connection by inserting 44 | it into your connection's request/response stack: 45 | 46 | ```ruby 47 | require 'faraday' 48 | require 'faraday/detailed_logger' 49 | 50 | connection = Faraday.new(url: 'http://sushi.com') do |faraday| 51 | faraday.request :url_encoded 52 | faraday.response :detailed_logger # <-- Inserts the logger into the connection. 53 | faraday.adapter Faraday.default_adapter 54 | end 55 | ``` 56 | 57 | By default, the Faraday::DetailedLogger will log to STDOUT. If this is not your 58 | desired log location, simply provide any Logger-compatible object as a 59 | parameter to the middleware definition: 60 | 61 | ```ruby 62 | require 'faraday' 63 | require 'faraday/detailed_logger' 64 | require 'logger' 65 | 66 | my_logger = Logger.new('logfile.log') 67 | my_logger.level = Logger::INFO 68 | 69 | connection = Faraday.new(url: 'http://sushi.com') do |faraday| 70 | faraday.request :url_encoded 71 | faraday.response :detailed_logger, my_logger # <-- sets a custom logger. 72 | faraday.adapter Faraday.default_adapter 73 | end 74 | ``` 75 | 76 | Or, perhaps use your Rails logger: 77 | 78 | ```ruby 79 | faraday.response :detailed_logger, Rails.logger 80 | ``` 81 | 82 | Further, you might like to tag logged output to make it easily located in your 83 | logs: 84 | 85 | ```ruby 86 | faraday.response :detailed_logger, Rails.logger, 'Sushi Request' 87 | ``` 88 | 89 | ### Example output 90 | 91 | Because logs generally work best with a single line of data per entry, the 92 | DEBUG-level output which contains the headers and bodies is inspected prior to 93 | logging. This crushes down and slightly manipulates the multi-line output one 94 | would expect when performing a verbose cURL operation into a log-compatible 95 | single line. 96 | 97 | Below is a contrived example showing how this works. Presuming cURL generated 98 | the following request and received the associated response: 99 | 100 | ```bash 101 | $ curl -v -d "requestbody=content" http://sushi.com/temaki 102 | > GET /temaki HTTP/1.1 103 | > User-Agent: Faraday::DetailedLogger 104 | > Host: sushi.com 105 | > Content-Type: application/x-www-form-urlencoded 106 | > 107 | > requestbody=content 108 | > 109 | < HTTP/1.1 200 OK 110 | < Content-Type: application/json 111 | < 112 | < {"order_id":"1"} 113 | ``` 114 | 115 | The Faraday::DetailedLogger would log something similar to the following, with 116 | DEBUG-level logging enabled: 117 | 118 | ```plain 119 | POST http://sushi.com/nigirizushi 120 | "User-Agent: Faraday::DetailedLogger\nContent-Type: application/x-www-form-urlencoded\n\nrequestbody=content" 121 | HTTP 200 122 | "Content-Type: application/json\n\n{\"order_id\":\"1\"}" 123 | ``` 124 | 125 | #### Request logging 126 | 127 | Log output for the request-portion of an HTTP interaction: 128 | 129 | ```plain 130 | POST http://sushi.com/temaki 131 | "User-Agent: Faraday v0.9.0\nAccept: application/json\nContent-Type: application/json\n\n{\"name\":\"Polar Bear\",\"ingredients\":[\"Tuna\",\"Salmon\",\"Cream Cheese\",\"Tempura Flakes\"],\"garnish\":\"Eel Sauce\"}" 132 | ``` 133 | 134 | The POST line is logged at an INFO level just before the request is transmitted 135 | to the remote system. The second line containing the request headers and body 136 | are logged at a DEBUG level. 137 | 138 | #### Response logging 139 | 140 | Log output for the response-portion of an HTTP interaction: 141 | Response portion: 142 | 143 | ```plain 144 | HTTP 202 145 | "server: nginx\ndate: Tue, 01 Jul 2014 21:56:52 GMT\ncontent-type: application/json\ncontent-length: 17\nconnection: close\nstatus: 202 Accepted\n\n{\"order_id\":\"1\"}" 146 | ``` 147 | 148 | The HTTP status line is logged at an INFO level at the same time the response 149 | is returned from the remote system. The second line containing the response 150 | headers and body are logged at a DEBUG level. 151 | 152 | ## Contributing 153 | 154 | Bug reports and pull requests are welcome on GitHub at https://github.com/envylabs/faraday-detailed_logger. 155 | 156 | ## License 157 | 158 | The gem is available as open source under the terms of the [MIT License][]. 159 | 160 | ### Dependency Licensing 161 | 162 | The dependencies and sub-dependencies of this gem are checked to be available 163 | for Private Use, Commercial Use, Modification, and Distribution: 164 | 165 | * [2-Clause BSD License][] 166 | * [Apache 2.0 License][] 167 | * [MIT License][] 168 | * [Ruby License][] 169 | 170 | [2-Clause BSD License]: https://opensource.org/licenses/BSD-2-Clause 171 | [Apache 2.0 License]: https://opensource.org/licenses/Apache-2.0 172 | [MIT License]: https://opensource.org/licenses/MIT 173 | [Ruby License]: https://en.wikipedia.org/wiki/Ruby_License 174 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task(default: :spec) 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'faraday/detailed_logger' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /doc/dependency_decisions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - - :permit 3 | - MIT 4 | - :who: Nathaniel Bibler 5 | :why: This license is FSF and OSI approved. 6 | :versions: [] 7 | :when: 2018-02-20 17:46:01.372600000 Z 8 | - - :permit 9 | - Simplified BSD 10 | - :who: Nathaniel Bibler 11 | :why: This license is FSF and OSI approved. 12 | :versions: [] 13 | :when: 2018-02-20 17:46:13.990005000 Z 14 | - - :permit 15 | - Ruby 16 | - :who: Nathaniel Bibler 17 | :why: This license is FSF approved. 18 | :versions: [] 19 | :when: 2018-02-20 17:46:30.088049000 Z 20 | - - :permit 21 | - Apache-2.0 22 | - :who: Nathaniel Bibler 23 | :why: This license is FSF and OSI approved. 24 | :versions: [] 25 | :when: 2018-02-20 17:46:53.048804000 Z 26 | -------------------------------------------------------------------------------- /faraday-detailed_logger.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/faraday/detailed_logger/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'faraday-detailed_logger' 7 | spec.version = Faraday::DetailedLogger::VERSION 8 | spec.authors = ['Envy Labs'] 9 | spec.email = [''] 10 | 11 | spec.summary = 'A detailed request and response logger for Faraday.' 12 | spec.description = %( 13 | A Faraday middleware for logging request and response activity including 14 | method, URI, headers, and body at varying log levels. 15 | ) 16 | spec.homepage = 'https://github.com/envylabs/faraday-detailed_logger' 17 | spec.license = 'MIT' 18 | spec.required_ruby_version = '>= 3.2' 19 | 20 | spec.metadata['bug_tracker_uri'] = 'https://github.com/envylabs/faraday-detailed_logger/issues' 21 | spec.metadata['changelog_uri'] = 'https://github.com/envylabs/faraday-detailed_logger/blob/main/CHANGELOG.md' 22 | spec.metadata['source_code_uri'] = 'https://github.com/envylabs/faraday-detailed_logger' 23 | 24 | # Specify which files should be added to the gem when it is released. 25 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 26 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 27 | `git ls-files -z`.split("\x0").reject do |f| 28 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|doc|gemfiles|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 29 | end 30 | end 31 | 32 | spec.require_paths = ['lib'] 33 | 34 | spec.add_runtime_dependency 'faraday', '>= 0.16', '< 3' 35 | 36 | spec.add_development_dependency 'appraisal', '~> 2.0' 37 | spec.add_development_dependency 'bundler', '~> 2.0' 38 | spec.add_development_dependency 'license_finder', '~> 7.0' 39 | spec.add_development_dependency 'rake', '~> 13.0' 40 | spec.add_development_dependency 'rspec', '~> 3.0' 41 | spec.add_development_dependency 'rubocop-rails_config', '~> 1.3' 42 | end 43 | -------------------------------------------------------------------------------- /gemfiles/faraday_0.16.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", "~> 0.16.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_0.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", "~> 0.16" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_1.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", "~> 1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_2.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", "~> 2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_canary.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", git: "https://github.com/lostisland/faraday.git" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_latest.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday/detailed_logger/version' 4 | require 'faraday/detailed_logger/middleware' 5 | 6 | require 'faraday' 7 | 8 | module Faraday 9 | module DetailedLogger 10 | end 11 | end 12 | 13 | Faraday::Response.register_middleware(detailed_logger: Faraday::DetailedLogger::Middleware) 14 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/curl_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module DetailedLogger 5 | module CurlFormatter 6 | def self.request(env) 7 | "#{env[:method].upcase} #{env[:url]}" 8 | end 9 | 10 | def self.request_body(env) 11 | curl_output(env[:request_headers], env[:body]).inspect 12 | end 13 | 14 | def self.response(env) 15 | "HTTP #{env[:status]}" 16 | end 17 | 18 | def self.response_body(env) 19 | curl_output(env[:response_headers], env[:body]).inspect 20 | end 21 | 22 | private 23 | 24 | def self.curl_output(headers, body) 25 | string = headers.to_a.sort_by(&:first).map { |k, v| "#{k}: #{v}" }.join("\n") 26 | string + "\n\n#{body}" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday' 4 | require 'faraday/detailed_logger/curl_formatter' 5 | require 'faraday/detailed_logger/tagged_logging' 6 | 7 | case 8 | when Gem::Dependency.new('', '> 2.0.0.alpha.pre.1').match?('', Faraday::VERSION) 9 | require_relative 'middleware/current' 10 | else 11 | require_relative 'middleware/legacy' 12 | end 13 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/middleware/current.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday' 4 | 5 | module Faraday 6 | module DetailedLogger 7 | # A Faraday middleware used for providing debug-level logging information. 8 | # The request and response logs follow very closely with cURL output for 9 | # ease of understanding. 10 | # 11 | # Be careful about your log level settings when using this middleware, 12 | # especially in a production environment. With a DEBUG level log enabled, 13 | # there will be potential information security concerns, because the 14 | # request and response headers and bodies will be logged out. At an INFO or 15 | # greater level, this is not a concern. 16 | # 17 | class Middleware < Faraday::Middleware 18 | attr_reader :logger 19 | attr_reader :tags 20 | 21 | # Internal: Used as the Middleware's logger in the case that an explicit 22 | # logger is not provided. 23 | # 24 | # Returns a Logger instance. 25 | # 26 | def self.default_logger 27 | require 'logger' 28 | ::Logger.new($stdout) 29 | end 30 | 31 | # Public: Initialize a new Logger middleware. 32 | # 33 | # app - A Faraday-compatible middleware stack or application. 34 | # logger - A Logger-compatible object to which the log information will 35 | # be recorded. 36 | # tags - An optional array of tags to apply to the log output. 37 | # 38 | # Returns a Logger instance. 39 | # 40 | def initialize(app, logger = nil, *tags) 41 | super(app) 42 | @formatter = CurlFormatter 43 | @logger = TaggedLogging.new(logger || self.class.default_logger) 44 | @tags = tags 45 | end 46 | 47 | # Public: Used by Faraday to execute the middleware during the 48 | # request/response cycle. 49 | # 50 | # env - A Faraday-compatible request environment. 51 | # 52 | # Returns the result of the parent application execution. 53 | # 54 | def call(env) 55 | super 56 | rescue 57 | logger.error do 58 | "#{$!.class.name} - #{$!.message} (#{$!.backtrace.first})" 59 | end 60 | raise 61 | end 62 | 63 | # Internal: Used by Faraday as a callback hook to process a network 64 | # response after it has completed. 65 | # 66 | # env - A Faraday-compatible response environment. 67 | # 68 | # Returns nothing. 69 | # 70 | def on_complete(env) 71 | status = env[:status] 72 | 73 | logger.tagged(*tags) do 74 | log_response_status(status) { formatter.response(env) } 75 | logger.debug { formatter.response_body(env) } 76 | end 77 | end 78 | 79 | # Public: Used by Faraday as a callback hook to process a network request 80 | # before it has been made. 81 | # 82 | # env - A Faraday-compatible request environment. 83 | # 84 | # Returns nothing. 85 | # 86 | def on_request(env) 87 | logger.tagged(*tags) do 88 | logger.info { formatter.request(env) } 89 | logger.debug { formatter.request_body(env) } 90 | end 91 | end 92 | 93 | private 94 | 95 | attr_reader :formatter 96 | 97 | def log_response_status(status, &block) 98 | case status 99 | when 200..399 100 | logger.info(&block) 101 | else 102 | logger.warn(&block) 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/middleware/legacy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday' 4 | 5 | module Faraday 6 | module DetailedLogger 7 | # A Faraday middleware used for providing debug-level logging information. 8 | # The request and response logs follow very closely with cURL output for 9 | # ease of understanding. 10 | # 11 | # Be careful about your log level settings when using this middleware, 12 | # especially in a production environment. With a DEBUG level log enabled, 13 | # there will be potential information security concerns, because the 14 | # request and response headers and bodies will be logged out. At an INFO or 15 | # greater level, this is not a concern. 16 | # 17 | class Middleware < Faraday::Response::Middleware 18 | attr_reader :logger 19 | attr_reader :tags 20 | 21 | # Internal: Used as the Middleware's logger in the case that an explicit 22 | # logger is not provided. 23 | # 24 | # Returns a Logger instance. 25 | # 26 | def self.default_logger 27 | require 'logger' 28 | ::Logger.new($stdout) 29 | end 30 | 31 | # Public: Initialize a new Logger middleware. 32 | # 33 | # app - A Faraday-compatible middleware stack or application. 34 | # logger - A Logger-compatible object to which the log information will 35 | # be recorded. 36 | # tags - An optional array of tags to apply to the log output. 37 | # 38 | # Returns a Logger instance. 39 | # 40 | def initialize(app, logger = nil, *tags) 41 | super(app) 42 | @formatter = CurlFormatter 43 | @logger = TaggedLogging.new(logger || self.class.default_logger) 44 | @tags = tags 45 | end 46 | 47 | # Public: Used by Faraday to execute the middleware during the 48 | # request/response cycle. 49 | # 50 | # env - A Faraday-compatible request environment. 51 | # 52 | # Returns the result of the parent application execution. 53 | # 54 | def call(env) 55 | logger.tagged(*tags) do 56 | logger.info { formatter.request(env) } 57 | logger.debug { formatter.request_body(env) } 58 | end 59 | super 60 | rescue 61 | logger.error do 62 | "#{$!.class.name} - #{$!.message} (#{$!.backtrace.first})" 63 | end 64 | raise 65 | end 66 | 67 | # Internal: Used by Faraday as a callback hook to process a network 68 | # response after it has completed. 69 | # 70 | # env - A Faraday-compatible response environment. 71 | # 72 | # Returns nothing. 73 | # 74 | def on_complete(env) 75 | status = env[:status] 76 | 77 | logger.tagged(*tags) do 78 | log_response_status(status) { formatter.response(env) } 79 | logger.debug { formatter.response_body(env) } 80 | end 81 | end 82 | 83 | private 84 | 85 | attr_reader :formatter 86 | 87 | def log_response_status(status, &block) 88 | case status 89 | when 200..399 90 | logger.info(&block) 91 | else 92 | logger.warn(&block) 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/tagged_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | module Faraday 6 | module DetailedLogger 7 | # This was largely lifted from ActiveSupport::TaggedLogging. Modifications 8 | # made to remove ActiveSupport dependencies (blank?, delegation, and 9 | # ActiveSupport::Logger). 10 | # 11 | module TaggedLogging 12 | extend Forwardable 13 | 14 | module Formatter 15 | BLANK = lambda do |value| 16 | value.respond_to?(:empty?) ? !!value.empty? : !value 17 | end 18 | 19 | def call(severity, timestamp, progname, msg) 20 | super(severity, timestamp, progname, "#{tags_text}#{msg}") 21 | end 22 | 23 | def tagged(*tags) 24 | new_tags = push_tags(*tags) 25 | yield self 26 | ensure 27 | pop_tags(new_tags.size) 28 | end 29 | 30 | def push_tags(*tags) 31 | tags.flatten.reject(&BLANK).tap do |new_tags| 32 | current_tags.concat new_tags 33 | end 34 | end 35 | 36 | def pop_tags(size = 1) 37 | current_tags.pop size 38 | end 39 | 40 | def clear_tags! 41 | current_tags.clear 42 | end 43 | 44 | def current_tags 45 | @thread_key ||= "faraday_detailed_logger_tags:#{object_id}" 46 | Thread.current[@thread_key] ||= [] 47 | end 48 | 49 | private 50 | 51 | def tags_text 52 | tags = current_tags 53 | if tags.any? 54 | tags.map { |tag| "[#{tag}] " }.join 55 | end 56 | end 57 | end 58 | 59 | def self.new(logger) 60 | unless logger.respond_to?(:tagged) 61 | logger.formatter ||= ::Logger::Formatter.new 62 | logger.formatter.extend Formatter 63 | logger.extend(self) 64 | end 65 | 66 | logger 67 | end 68 | 69 | def tagged(*tags) 70 | formatter.tagged(*tags) { yield self } 71 | end 72 | 73 | def flush 74 | clear_tags! 75 | super if defined?(super) 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/faraday/detailed_logger/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module DetailedLogger 5 | VERSION = '2.6.1' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "bump-minor-pre-major": true, 5 | "bump-patch-for-minor-pre-major": false, 6 | "changelog-path": "CHANGELOG.md", 7 | "component": "", 8 | "include-component-in-tag": false, 9 | "draft": false, 10 | "monorepo-tags": false, 11 | "package-name": "faraday-detailed_logger", 12 | "prerelease": false, 13 | "release-type": "ruby", 14 | "version-file": "lib/faraday/detailed_logger/version.rb" 15 | } 16 | }, 17 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 18 | } -------------------------------------------------------------------------------- /spec/faraday/detailed_logger/curl_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::DetailedLogger::CurlFormatter do 4 | context '#request' do 5 | it 'returns the HTTP verb and URL' do 6 | env = Faraday::Env.new 7 | env[:method] = 'GET' 8 | env[:url] = 'http://sushi.com/temaki' 9 | 10 | expect(described_class.request(env)).to eq('GET http://sushi.com/temaki') 11 | end 12 | end 13 | 14 | context '#request_body' do 15 | it 'returns a cURL-like request structure' do 16 | env = Faraday::Env.new 17 | env[:body] = 'body=content' 18 | env[:method] = 'POST' 19 | env[:request_headers] = { 20 | 'Content-Type' => 'application/x-www-form-urlencoded', 21 | 'User-Agent' => 'Faraday::DetailedLogger' 22 | } 23 | env[:url] = 'http://sushi.com/temaki' 24 | 25 | expected = <<~EXPECTED.strip.inspect 26 | Content-Type: application/x-www-form-urlencoded 27 | User-Agent: Faraday::DetailedLogger 28 | 29 | body=content 30 | EXPECTED 31 | 32 | expect(described_class.request_body(env)).to eq(expected) 33 | end 34 | end 35 | 36 | context '#response' do 37 | it 'returns the HTTP status code' do 38 | env = Faraday::Env.new 39 | env[:status] = 200 40 | 41 | expect(described_class.response(env)).to eq('HTTP 200') 42 | end 43 | end 44 | 45 | context '#response_body' do 46 | it 'returns a cURL-like response structure' do 47 | env = Faraday::Env.new 48 | env[:body] = '{"id":"1"}' 49 | env[:response_headers] = { 'Content-Type' => 'application/json' } 50 | 51 | expected = <<~EXPECTED.strip.inspect 52 | Content-Type: application/json 53 | 54 | {"id":"1"} 55 | EXPECTED 56 | 57 | expect(described_class.response_body(env)).to eq(expected) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/faraday/detailed_logger/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | require 'stringio' 5 | 6 | RSpec.describe Faraday::DetailedLogger::Middleware do 7 | TestError = Class.new(StandardError) 8 | 9 | let(:log) { StringIO.new } 10 | let(:logger) { Logger.new(log) } 11 | 12 | context 'by default' do 13 | it 'logs to STDOUT' do 14 | expect { connection(nil).get('/temaki') }.to output.to_stdout 15 | end 16 | end 17 | 18 | context 'with tags configured' do 19 | it 'logs prepends the tags to each line' do 20 | connection(logger, %w[ebi]).get('/temaki') 21 | log.rewind 22 | log.readlines.each do |line| 23 | expect(line).to match(/: \[ebi\] /) 24 | end 25 | end 26 | end 27 | 28 | context 'for the HTTP request-portion' do 29 | it 'logs request method and URI at an INFO level' do 30 | connection(logger).get('/temaki') 31 | log.rewind 32 | expect(log.read).to match(%r{\bINFO\b.+\bGET http://sushi\.com/temaki\b}) 33 | end 34 | 35 | it 'logs the requested body at a DEBUG level' do 36 | connection(logger).post( 37 | '/nigirizushi', 38 | { 'body' => 'content' }, 39 | { user_agent: 'Faraday::DetailedLogger' } 40 | ) 41 | log.rewind 42 | curl = <<~CURL.strip 43 | Content-Type: application/x-www-form-urlencoded 44 | User-Agent: Faraday::DetailedLogger 45 | 46 | body=content 47 | CURL 48 | expect(log.read).to match(/\bDEBUG\b.+#{Regexp.escape(curl.inspect)}/) 49 | end 50 | end 51 | 52 | context 'for the HTTP response-portion' do 53 | it 'logs a 2XX response status code at an INFO level' do 54 | connection(logger).get('/oaiso', c: 200) 55 | log.rewind 56 | expect(log.read).to match(/\bINFO\b.+\bHTTP 200\b/) 57 | end 58 | 59 | it 'logs a 3XX response status code at an INFO level' do 60 | connection(logger).get('/oaiso', c: 301) 61 | log.rewind 62 | expect(log.read).to match(/\bINFO\b.+\bHTTP 301\b/) 63 | end 64 | 65 | it 'logs a 4XX response status code at a WARN level' do 66 | connection(logger).get('/oaiso', c: 401) 67 | log.rewind 68 | expect(log.read).to match(/\bWARN\b.+\bHTTP 401\b/) 69 | end 70 | 71 | it 'logs a 5XX response status code at an WARN level' do 72 | connection(logger).get('/oaiso', c: 500) 73 | log.rewind 74 | expect(log.read).to match(/\bWARN\b.+\bHTTP 500\b/) 75 | end 76 | 77 | it 'logs the response headers and body at a DEBUG level' do 78 | connection(logger).post('/nigirizushi') 79 | log.rewind 80 | curl = <<~CURL.strip 81 | Content-Type: application/json 82 | 83 | {"id":"1"} 84 | CURL 85 | expect(log.read).to match(/\bDEBUG\b.+#{Regexp.escape(curl.inspect)}/) 86 | end 87 | 88 | it 'logs errors which occur during the request and re-raises them' do 89 | logger = Logger.new(log = StringIO.new) 90 | 91 | expect do 92 | connection(logger).get('/error') 93 | end.to raise_error(TestError, 'An error occurred during the request') 94 | 95 | log.rewind 96 | expect(log.read).to match( 97 | /\bERROR\b.+\bTestError - An error occurred during the request \(.+\)$/ 98 | ) 99 | end 100 | end 101 | 102 | private 103 | 104 | def connection(logger = nil, *tags) 105 | Faraday.new(url: 'http://sushi.com') do |builder| 106 | builder.request(:url_encoded) 107 | builder.response(:detailed_logger, logger, *tags) 108 | builder.adapter(:test) do |stub| 109 | stub.get('/temaki') do 110 | [200, { 'Content-Type' => 'text/plain' }, 'temaki'] 111 | end 112 | stub.post('/nigirizushi') do 113 | [200, { 'Content-Type' => 'application/json' }, '{"id":"1"}'] 114 | end 115 | stub.get('/oaiso') do |env| 116 | code = env.respond_to?(:params) ? env.params['c'] : env[:params]['c'] 117 | [code.to_i, { 'Content-Type' => 'application/json' }, code] 118 | end 119 | stub.get('/error') do 120 | raise TestError, 'An error occurred during the request' 121 | end 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib', __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | require 'faraday/detailed_logger' 8 | 9 | RSpec.configure do |config| 10 | Kernel.srand config.seed 11 | 12 | config.disable_monkey_patching! 13 | config.example_status_persistence_file_path = '.rspec_status' 14 | config.filter_run :focus 15 | config.order = :random 16 | config.profile_examples = 10 17 | config.run_all_when_everything_filtered = true 18 | 19 | if config.files_to_run.one? 20 | config.default_formatter = 'doc' 21 | end 22 | 23 | config.expect_with :rspec do |expectations| 24 | expectations.syntax = :expect 25 | end 26 | 27 | config.mock_with :rspec do |mocks| 28 | mocks.syntax = :expect 29 | mocks.verify_partial_doubles = true 30 | end 31 | end 32 | --------------------------------------------------------------------------------