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

Reimplement TaggedLogging: #49771

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Edouard-chin
Copy link
Member

@Edouard-chin Edouard-chin commented Oct 24, 2023

  • Context

    The Tagged Logging functionality has been a source of a few issues over the years, especially when combined with the broadcasting feature. Initializating a Tagged Logger was also not very intuitive:

    logger = Logger.new(STDOUT)
    tagged_logger = ActiveSupport::TaggedLogging.new(logger)
    # Would expect the `tagged_logger` to be an instance of `AS::TaggedLogging` 
    # but it's a clone of the `logger`.
    
    tagged_logger.formatter = ->(_, _, _, message) do
      { message: message }
    end
    # Modifies the formatter to output JSON formatted logs.
    # This breaks tagged logging.

    I believe the main reason of those issues is because tagged logging is implemented at the wrong level.

    Solution

    I made a proposal on the Ruby logger upstream in New feature to allow adding extra information to log ruby/logger#90 to help solve this but it hasn't been reviewed yet. So I thought about adding it here for now. The TL;DR is to decouple formatting and adding extra information to logs (which is what tagged logging does).

    Deprecation

    Since TaggedLogging will no longer access the formatter, there is a few things I'd like to deprecate such as setting a default formatter

    logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
    but doing so in this PR would increase the size of the diff significantly and would maybe distract for PR reviews.

    Another thing that I believe should be deprecated is ActiveSupport::TaggedLogging.new. Adding tagging functionality to a logger should be done using a more ruby approach such as logger.extend(AS::TaggedLogging).


Fix #49757
Fix #49745
Fix #46084
Fix #44668

assert_equal("[BMX] Hello\n", @sink2.string)
end

test "tag logs for the whole broadcast without a block" do
Copy link
Member Author

Choose a reason for hiding this comment

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

Regression test for #49757 and #49745

@broadcast.broadcast_to(@logger1, @logger2)
end

test "tag logs for the whole broadcast with a block" do
Copy link
Member Author

Choose a reason for hiding this comment

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

Regression test for #49745 and #44668

Comment on lines +33 to +35
if logger.formatter.nil?
logger.formatter ||= Logger::SimpleFormatter.new
end
Copy link
Member Author

Choose a reason for hiding this comment

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

Added this for backward compatibility (otherwise logs would be formatted differently). I'd like to deprecate it though (see PR description if that also makes sense to you).

assert_equal("[BMX] [BROADCAST2] Hello\n", @sink4.string)
end

test "#silence works while broadcasting to tagged loggers" do
Copy link
Member Author

Choose a reason for hiding this comment

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

@davidstosik I saw your comment on #49745 (comment) , but I wasn't able to reproduce the bug.

I wrote this test which is passing, let me know if this is similar to what you had in mind.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's what I had in mind.
I think I was experiencing issues with #silence because I was experimenting with my own implementation of BroadcastLogger that did not include LoggerSilence.

I wonder, if I have two loggers in my broadcast with different severity levels, will silence have an unexpected impact of those loggers' respective level? (Eg. equalize them.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: I confirmed that it doesn't look like there is any problem:

logger = ActiveSupport::BroadcastLogger.new(
  ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout)),
  ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout)).tap { _1.level = :warn },
)

puts "levels= " + logger.broadcasts.map(&:level).join(",")

logger.silence do
  logger.warn("warning")
  logger.info("info")
  puts "in logger.silence"
end

puts "levels= " + logger.broadcasts.map(&:level).join(",")

# Output:
#   levels= 0,2
#   in logger.silence # (only once 👍🏻)
#   levels= 0,2
# (no other unexpected log)

Copy link
Contributor

Choose a reason for hiding this comment

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

@Edouard-chin I'm having more doubts about #silence (I guess unrelated to this PR, but as we started the conversation here...)

require "active_support/isolated_execution_state"
require "active_support/logger_silence"
require "active_support/broadcast_logger"

logger = ActiveSupport::BroadcastLogger.new(Logger.new(STDOUT), Logger.new(STDOUT))
logger.silence { logger.warn("Error") };
# W, [2023-10-27T11:14:11.425871 #59954]  WARN -- : Error
# W, [2023-10-27T11:14:11.425960 #59954]  WARN -- : Error

Should silencing a BroadcastLogger silence all loggers it broadcasts to, or should it silence only the loggers that support silencing?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good question. I'd have expect it to silence all logs, no matter if the loggers supports silencing. Worth noting that the behaviour now is the same as before:

# Rails 7.0
Rails.logger.extend(ActiveSupport::Logger.broadcast(Logger.new(STDOUT))

Rails.logger.silence { Rails.logger.info("hello") }
# I, [2023-10-27T14:00:18.659473 #86197]  INFO -- : hello

Maybe we can change this behaviour in another PR and silence the whole broadcast. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm definitely not asking it to be done in this PR! 👍🏻
When it comes to PRs, the smaller the better. 👌🏻

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yeah no worries! I was just wondering if silencing the whole broadcast is the behaviour you would expect and whether we should change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that would be my own expectation too. BroadcastLogger is trying to be "transparent" to the user, so doing Rails.logger.silence should, without surprise, stop Rails.logger.log from outputting anything on all channels.
Be careful though that LoggerSilence achieves that by temporarily assigning a different log level (using log_at). Doing so on the BroadcastLogger itself could make it impossible to restore each logger to its original level.

logger.formatter.extend LocalTagStorage
logger.push_tags(*formatter.current_tags, *tags)
logger = clone
logger.tag_processor.extend(LocalTagStorage)
Copy link

Choose a reason for hiding this comment

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

hi, could you also resolve the issue 48787 ?
the LocalTagStorage is cause that TagStack is leaked across threads because it prevents the Formatter module (you change to class TagProcessor) to get the IsolatedExecutionState TagStack in each separate thread.
I wonder what the purpose of this LocalTagStorage anyway ?

The issue 48787 also raise the confusion about when shared tagged-logs across threads and when not, could you also introduce a proposal to clear this confusion also in this PR ?
Perhaps we could introduce a based_tagged to store all share-tagged which will use acrcoss threads, so TagStack in each thread will be based_tagged + IsolatedExecutionState TagStack.
what do you think ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hey! I'd have to dig on #48787 and figure out the original intent to share tags across threads (thought from a quick glance this seems to be on purpose).
This PR is big enough and the mentioned issue isn't really related to the changes I'm proposing here, so this will potentially be changed in another patch.

# configured in such a way).
# Tagging your logs can be done for the whole broadcast or for each sink independently.
#
# Tagging logs for the whole broadcast
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Tagging logs for the whole broadcast
# Tagging logs for the whole broadcast:

#
# Outputs: "[BMX] Hello world!" is written on both STDOUT and in the file.
#
# Tagging logs for a single logger
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Tagging logs for a single logger
# Tagging logs for a single logger:

# Outputs: "[BMX] Hello world!" is written on STDOUT
# Outputs: "Hello world!" is written in the file
#
# Adding tags for the whole broadcast and adding extra tags on a specific logger
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Adding tags for the whole broadcast and adding extra tags on a specific logger
# Adding tags for the whole broadcast and adding extra tags on a specific logger:

@@ -231,6 +269,17 @@ def dispatch(&block)
@broadcasts.each { |logger| block.call(logger) }
end

def dispatch_with_processors(&block)
@broadcasts.each do |logger|
logger.extend(LogProcessor) unless logger.is_a?(LogProcessor)
Copy link
Member

Choose a reason for hiding this comment

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

Can we extend the logger when creating the broadcast? Otherwise we will invalidate the method cache at runtime instead of boottime.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, good point 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved this to the broadcast_to method (which gets called when creating the broadcast).
This is to support adding loggers to the broadcast after initialisation and keep the broadcasting functionality working.

@rafaelfranca rafaelfranca added this to the 7.1.4 milestone Jan 16, 2024
- ### Context

  The Tagged Logging functionality has been a source of a few
  issues over the years, especially when combined with the
  broadcasting feature.
  Initializating a Tagged Logger wasn't very intuitive:

  ```ruby
    logger = Logger.new(STDOUT)
    tagged_logger = ActiveSupport::TaggedLogging.new(logger)
    # Would expect the `tagged_logger` to be an instance of `AS::TaggedLogging`
    # but it's a clone of the `logger`.

    tagged_logger.formatter = ->(_, _, _, message) do
      { message: message }
    end
    # Modifies the formatter to output JSON formatted logs.
    # This breaks tagged logging.
  ```

  I believe the main reason of those issues is because tagged logging
  is implemented at the wrong level.

  ### Solution

  I made a proposal on the Ruby logger upstream in ruby/logger#90 to help
  solve this but it hasn't been reviewed yet. So I thought about adding
  it here for now.
  The TL;DR is to decouple formatting and adding extra information to
  logs (which is what tagged logging does).

  ### Deprecation

  Since TaggedLogging will no longer access the formatter, there is a
  few things I'd like to deprecate (such as setting a default
  formatter https://github.com/rails/rails/blob/d68e43922bc11829c52ad9f736ad5549fc97631b/activesupport/lib/active_support/tagged_logging.rb#L124)
  but doing so in this PR would increase the size of the diff
  significantly and would maybe distract for PR reviews.

  Another thing that I believe should be deprecated is
  `ActiveSupport::TaggedLogging.new`. Adding tagging
  functionality to a logger should be done using
  a more ruby approach such as `logger.extend(AS::TaggedLogging)`.

  Fix rails#49757
  Fix rails#49745
  Fix rails#46084
  Fix rails#44668

I made a propose on the Ruby logger upstream in ruby/logger#90,
  but it hasn't been reviewed it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants