Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails 7.1: Record only one copy of a broadcasted log message #2252

Merged
merged 4 commits into from
Oct 13, 2023

Conversation

kaylareopelle
Copy link
Contributor

In Rails, you have one central logger, Rails.logger, but often want those messages to be output to multiple destinations. In a development environment, this includes STDOUT and development.log by default. This works by having one central logger that creates the logs, and "broadcasts" those logs to multiple destinations.

This posed a problem when we added APM automatic log forwarding, because New Relic emitted an identical log event for each broadcast, often causing duplicate events to appear in the UI.

In earlier versions of Rails, we leverage ActiveSupport::Logger.broadcast to set the @skip_instrumenting instance variable to true to avoid recording duplicated logs. This was a private method, prone to change at any time.

Rails 7.1 replaced the ActiveSupport::Logger.broadcast method with a new public class, ActiveSupport::BroadcastLogger. This class operates in largely the same way as the old method, but since it is a class and no longer a method, we cannot monkey-patch it in the same way. However, since it is public, we can have greater faith in the stability of our instrumentation.

This POC provides one option to prevent duplicate log records from being sent to New Relic when using ActiveSupport::BroadcastLogger. Logs emitted by debug, info, or add methods set @skip_instrumenting to true for all but the first logger in the broadcasts array. At the end of the method, all loggers in the broadcasts array have @skip_instrumenting set to false.

Some things to consider:

  • The loggers included in ActiveSupport::BroadcastLogger#broadcasts can change after startup. New loggers may be added or removed from the array.
  • The add method in ActiveSupport::BroadcastLogger calls the add method on each individual logger. This means the BroadcastLogger version of the method doesn't call the severity method (ex. debug) like it does in the Ruby logger.
  • If we decide this POC is on the right track, we'll need to monkey-patch all the severity methods to get our desired behavior (error, warn, etc.)

Closes: #2245

Broadcasted loggers have unique outputs, but identical inputs. If our logger
instrumentation receives logs from broadcasted loggers, it creates identical
log events from each broadcast.

In Rails versions below 7.1, the duplicated log events were avoided by
adding an instance variable to skip instrumentation on broadcasted
loggers by patching ActiveSupport::Logger.broadcast.

That method has been refactored into the ActiveSupport::BroadcastLogger
class in Rails 7.1.

This delivers updates to prevent duplicate log events in
ActiveSupport::BroadcastLogger and updates testing for ActiveSupport::Logger
to test chain instrumentation. It also adds a test using Rails logger
directly, so we can be notified of any regressions by testing future
versions of Rails.
@kaylareopelle kaylareopelle force-pushed the fix-active-support-logger-instrumentation branch from 8d55b4e to bd8a857 Compare October 11, 2023 22:10
@@ -2,7 +2,7 @@

## dev

Version <dev> brings support for gleaning a Docker container id from cgroups v2 based containers and records additional synthetics attributes.
Version <dev> brings support for gleaning a Docker container id from cgroups v2 based containers, records additional synthetics attributes, and fixes an issue with Rails 7.1 that could cause duplicate log records to be sent to New Relic.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to have suggestions on how to improve this and the statement below, especially if we want to turn it into a more general statement about the agent supporting Rails 7.1.

I also noticed the active_support_logger instrumentation wasn't specifically called out in the changelog when we released logs functionality, so what we reveal here may not need to be so specific.

DependencyDetection.defer do
named :'active_support_broadcast_logger'

depends_on { defined?(ActiveSupport::BroadcastLogger) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should prevent the BroadcastLogger instrumentation from being installed on Rails versions below 7.1.

::ActiveSupportBroadcastLogger.class_eval do
include NewRelic::Agent::Instrumentation::ActiveSupportBroadcastLogger

alias_method(:add_without_new_relic, :add)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In both chain and prepend, I tried to use iteration and define_method to reduce duplication in the files. Unfortuantely, super can't be called within define_method.

example prepend attempt:

LOG_RECORD_METHODS = %w[add debug info warn error fatal unknown]

LOG_RECORD_METHODS.each do |method|
  define_method "#{method}" do |*args|
    skip_broadcasts_with_new_relic(*args) { super }
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For others who might come across this with curiosity... define_method works fine with super if the arguments are fixed. But the splatted *args needed here won't work:

implicit argument passing of super from method defined by
define_method() is not supported. Specify all arguments
explicitly. (RuntimeError)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarifying, @fallwith!

# Issue #2245
defined?(Rails::VERSION::STRING) &&
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new('7.1.0')
defined?(ActiveSupport::Logger) && defined?(ActiveSupport::Logger.broadcast)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActiveSupport::Logger is still defined in Rails 7.1, but the method we instrument, ActiveSupport::Logger.broadcast is not. This seemed more precise than using a Rails version.

@@ -0,0 +1,23 @@
# This file is distributed under New Relic's license terms.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized our chain instrumentation for ActiveSupport::Logger was never being tested, so I created a multiverse suite to make sure it gets checked.

@@ -0,0 +1,43 @@
# This file is distributed under New Relic's license terms.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully, this test will give us a heads-up if something changes with the Rails logger that causes the duplication issue to resurface.

@kaylareopelle kaylareopelle changed the title WIP: Ignore all broadcasts except one Rails 7.1: Record only one copy of a broadcasted log message Oct 11, 2023
@kaylareopelle kaylareopelle marked this pull request as ready for review October 11, 2023 22:13
fallwith
fallwith previously approved these changes Oct 11, 2023
fallwith
fallwith previously approved these changes Oct 11, 2023
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require './app'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equivalent to require_relative 'app' with fewer keystrokes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! I prefer require_relative. This was a copy/paste carryover.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in: 0f9ebe3

CHANGELOG.md Outdated Show resolved Hide resolved
@github-actions
Copy link

SimpleCov Report

Coverage Threshold
Line 94.06% 94%
Branch 85.65% 85%

@kaylareopelle kaylareopelle merged commit 67565e8 into dev Oct 13, 2023
26 checks passed
@kaylareopelle kaylareopelle deleted the fix-active-support-logger-instrumentation branch October 13, 2023 19:35
@nathan-appere
Copy link
Contributor

nathan-appere commented Mar 14, 2024

@kaylareopelle this causes an exception for us on:

NoMethodError
undefined method `each' for nil:NilClass (NoMethodError)

      broadcasts[1..-1].each { |broadcasted_logger| broadcasted_logger.instance_variable_set(:@skip_instrumenting, true) }

With:

  • newrelic_rpm (9.7.1)
  • rails (7.1.3.2)

I have no idea how broadcasts can be nil (should be an Array looking at the implementation of ActiveSupportBroadcastLogger)

Only happens in production with low volume (1 failure every 10000 calls), so tricky to identify the cause (the volume makes me think it could be somehow thread related?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Rails 7.1: Duplicate log events created per broadcasted logger
3 participants