From 1c08eeb97e21358a823dfa300f806f9ed8a95c8f Mon Sep 17 00:00:00 2001 From: Arne De Herdt Date: Fri, 16 Sep 2022 11:49:06 +0200 Subject: [PATCH] [CIOPS-1085] Silence log requests This pull request introduces the silencing of the Rack and Rails logger for the endpoint. By default we will not log any of the requests to not spam the output and log files with repeated simple checks. There is an option to enable to when desired. --- CHANGELOG.md | 4 +++ lib/is_it_ready.rb | 6 ++++ lib/is_it_ready/engine.rb | 6 ++++ lib/is_it_ready/log_silencer.rb | 41 ++++++++++++++++++++++ lib/is_it_ready/version.rb | 2 +- test/integration/custom_navigation_test.rb | 10 +++++- test/integration/logging_test.rb | 31 ++++++++++++++++ test/integration/silent_logging_test.rb | 21 +++++++++++ test/support/logger_introspection.rb | 22 ++++++++++++ 9 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 lib/is_it_ready/log_silencer.rb create mode 100644 test/integration/logging_test.rb create mode 100644 test/integration/silent_logging_test.rb create mode 100644 test/support/logger_introspection.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb9a6c..51efcc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Is It Ready? CHANGELOG +## 0.0.5 +* Silence Rails and the Rack middleware by default +* Add an option to allow logging of requests + ## 0.0.4 * Add support for HTTP Authorization Bearer tokens diff --git a/lib/is_it_ready.rb b/lib/is_it_ready.rb index 5a6b2e3..02f34e4 100644 --- a/lib/is_it_ready.rb +++ b/lib/is_it_ready.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'is_it_ready/log_silencer' require "is_it_ready/engine" # The namespace holding everything related to our Gem. @@ -19,4 +20,9 @@ module IsItReady # When enabled however, the request will need to provide the required token as a Bearer value # in the AUTHORIZATION header of the request. mattr_accessor :bearer_token + + # Silences the logging of the request against the endpoint. Defaults to true. + # When disabled, the entire request will appear in the Rails logs. + mattr_accessor :silence_logs + @@silence_logs = true end diff --git a/lib/is_it_ready/engine.rb b/lib/is_it_ready/engine.rb index 2a02e8c..608d2b1 100644 --- a/lib/is_it_ready/engine.rb +++ b/lib/is_it_ready/engine.rb @@ -14,5 +14,11 @@ class Engine < ::Rails::Engine mount ::IsItReady::Engine => ::IsItReady.endpoint end end + + # If the user has enabled the silencing of the loggers, we will mount the middleware + # to do so, otherwise skip the process entirely. + initializer 'is_it_ready.add_middleware' do |app| + app.middleware.insert_before(::Rails::Rack::Logger, ::IsItReady::LogSilencer, silenced: ::IsItReady.endpoint) + end end end diff --git a/lib/is_it_ready/log_silencer.rb b/lib/is_it_ready/log_silencer.rb new file mode 100644 index 0000000..91c54d0 --- /dev/null +++ b/lib/is_it_ready/log_silencer.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module IsItReady + # This class is a Rack::Middleware implementation that will support us in silencing the + # logging of calls for incoming requests against the endpoint. Otherwise the app would be + # writing all requests in the Rails logs, causing an overload of information to be reported + # that's simply not relevant. The usage of this Middleware can be controlled through the Engine's + # configuration on whether to silence logging or not. + class LogSilencer + # Creates a new instance of the Middleware and initializes it using the Rack standard approach + # for setting up the required values in a Rack::Middleware. + def initialize(app, opts = {}) + @silenced = opts.delete(:silenced) + @app = app + end + + # Executes the Middleware. + # If the environment contains the special X-SILENCE-LOGGER header to globally silence the request, + # or the path matches the provided silence configuration, the middleware will silence Rails for the + # request, otherwise pass the request along. + def call(env) + if ::IsItReady.silence_logs && silence_path?(env['PATH_INFO']) + ::Rails.logger.silence do + @app.call(env) + end + else + @app.call(env) + end + end + + private + + # Returns true when the given path needs to be silenced. + # This uses a manual Regex check, since the .match? method might not exist depending on the Ruby + # version that's being used for this gem. So we perform a manual match and return true if there's + # a 0 response. + def silence_path?(path) + (path =~ /#{@silenced}/).present? + end + end +end diff --git a/lib/is_it_ready/version.rb b/lib/is_it_ready/version.rb index ea8cb1c..40a90de 100644 --- a/lib/is_it_ready/version.rb +++ b/lib/is_it_ready/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module IsItReady - VERSION = '0.0.4' + VERSION = '0.0.5' end diff --git a/test/integration/custom_navigation_test.rb b/test/integration/custom_navigation_test.rb index f02b9d1..7ae1743 100644 --- a/test/integration/custom_navigation_test.rb +++ b/test/integration/custom_navigation_test.rb @@ -1,12 +1,20 @@ require 'test_helper' module IsItReady + # This test verifies whether the Engine respects the dynamic configuration of the endpoint, + # allowing it to be dynamically loaded when Rails loads the configuration, ensuring that we can + # mount the engine using a custom path when required. class CustomNavigationTest < ActionDispatch::IntegrationTest include Engine.routes.url_helpers setup do ::IsItReady.endpoint = '/something_else' - Rails.application.reload_routes! + ::Rails.application.reload_routes! + end + + teardown do + ::IsItReady.endpoint = ::IsItReady::DEFAULT_PATH + ::Rails.application.reload_routes! end test('it returns the correct response status on the root') do diff --git a/test/integration/logging_test.rb b/test/integration/logging_test.rb new file mode 100644 index 0000000..0b8e688 --- /dev/null +++ b/test/integration/logging_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +module IsItReady + # This class tests whether the ::Rails.logger is properly enabled during the execution of the Rails.engine. + # By default silencing of the incoming requests on the health check is disabled to avoid the Rails logs + # from being spammed with the repeated health checks. With this test, we check whether the Engine + # respects the configuration at runtime. + class SilentLoggingTest < ::ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + include ::LoggerIntrospection + + setup do + ::IsItReady.endpoint = ::IsItReady::DEFAULT_PATH + ::IsItReady.silence_logs = false + end + + teardown do + ::IsItReady.silence_logs = true + end + + test('it writes the request to the standard Rails logger') do + with_logger_introspection do |logger_output| + get root_url + + assert_match(/Started GET "#{::IsItReady.endpoint}\/" for 127.0.0.1 at */, logger_output.string) + end + end + end +end diff --git a/test/integration/silent_logging_test.rb b/test/integration/silent_logging_test.rb new file mode 100644 index 0000000..7a45d48 --- /dev/null +++ b/test/integration/silent_logging_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'test_helper' + +module IsItReady + # This class tests whether the ::Rails.logger is properly silenced during the execution of the Rails.engine. + # By default silencing of the incoming requests on the health check is disabled to avoid the Rails logs + # from being spammed with the repeated health checks. + class SilentLoggingTest < ::ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + include ::LoggerIntrospection + + test('it does not write anything to the standard Rails logger') do + with_logger_introspection do |logger_output| + get root_url + + refute_match(/Started GET "#{::IsItReady.endpoint}" for 127.0.0.1 at */, logger_output.string) + end + end + end +end diff --git a/test/support/logger_introspection.rb b/test/support/logger_introspection.rb new file mode 100644 index 0000000..b0b53d3 --- /dev/null +++ b/test/support/logger_introspection.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# This module contains methods that can be used in tests to perform introspection on the Rails logger. +# This allows us to wrap some support functionality inside our tests and check whether certain behavior +# is implemented correctly without overloading the test itself. +module LoggerIntrospection + # Performs introspection on the ::Rails.logger with the given block inside the test. + # The method will duplicate the original logger, and replace the logger with a simple StringIO object. + # All log entries are then made available in the object, and after the test the logger is restored + # to the original functionality to not affect other tests. + def with_logger_introspection(&block) + original_logger = ::Rails.logger.dup + @logger_output = ::StringIO.new + + begin + ::Rails.logger = ::ActiveSupport::Logger.new(@logger_output) + block.call(@logger_output) + ensure + ::Rails.logger = original_logger + end + end +end