-
Notifications
You must be signed in to change notification settings - Fork 599
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
Conversation
lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb
Outdated
Show resolved
Hide resolved
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.
8d55b4e
to
bd8a857
Compare
@@ -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. |
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
require './app' |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in: 0f9ebe3
SimpleCov Report
|
@kaylareopelle this causes an exception for us on:
With:
I have no idea how 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?) |
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 anddevelopment.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 totrue
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 bydebug
,info
, oradd
methods set@skip_instrumenting
totrue
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 tofalse
.Some things to consider:
ActiveSupport::BroadcastLogger#broadcasts
can change after startup. New loggers may be added or removed from the array.add
method inActiveSupport::BroadcastLogger
calls theadd
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.error
,warn
, etc.)Closes: #2245