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

Add a public API for broadcasting logs #48615

Merged
merged 1 commit into from Sep 25, 2023
Merged

Conversation

Edouard-chin
Copy link
Member

@Edouard-chin Edouard-chin commented Jun 30, 2023

Context

While working on #44695, I realised that Broadcasting was still a private API, although it’s commonly used. @rafaelfranca mentioned that making it public would require some refactor because of the original implementation which was hard
to understand and maintain.

Broadcasting

Broadcasting in a nutshell worked by “transforming” an existing logger into a broadcasted one.
The logger would then be responsible to log and format its own messages as well as passing the message along to other logger it broadcasts to.

The problem with this approach was the following:

  • Heavy use of metaprogramming.

  • Accessing the loggers in the broadcast wasn’t possible. Removing a logger from the broadcast either.

  • More importantly, modifying the main logger (the one that broadcasts logs to the others) wasn’t possible and the main source of misunderstanding.

      logger = Logger.new(STDOUT)
      stderr_logger = Logger.new(STDER))
      logger.extend(AS::Logger.broadcast(stderr_logger))
    
      logger.level = DEBUG # This modifies the level on all other loggers
      logger.formatter =  # Modified the formatter on all other loggers

To keep the contract unchanged on what Rails.logger returns, the new BroadcastLogger class implement duck typing with all methods that has the vanilla Ruby Logger class.
It's a simple and boring PORO that keeps an array of all the loggers that are part of the broadcast and iterate over whenever a log is sent.

Now, users can access all loggers inside the broadcast and modify them on the fly. They can also remove any logger from the broadcast at any time.

# Before

stdout_logger = Logger.new(STDOUT)
stderr_logger = Logger.new(STDER)
file_logger = Logger.new(“development.log”)
stdout_logger.extend(AS::Logger.broadcast(stderr_logger))
stdout_logger.extend(AS::Logger.broadcast(file_logger))

# After

broadcast = BroadcastLogger.new(stdout_logger, stderr_logger, file_logger)

I also think that now, it should be more clear for users that the broadcast sole job is to pass everything to the whole loggers in the broadcast. So there should be no surprise that all loggers in the broadcast get their level modified when they call broadcast.level = DEBUG .

It’s also easier to wrap your head around more complex setup such as broadcasting logs to another broadcast: broadcast.broadcast_to(stdout_logger, other_broadcast)

# @return [Array<Logger>] All the logger that are part of this broadcast.
attr_reader :broadcasts

def initialize(logdev = File::NULL, *args, **kwargs)
Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally would have preferred to be able to do BroadcastLogger.new(logger1, logger2), but that would mean either:

  • Modify the original signature
  • Create a PORO and implement duck typing because I felt the BroadcastLogger should behave just like a logger.

Not sure what's the best option

Copy link
Member

Choose a reason for hiding this comment

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

I think we should do the latter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good thanks. So I guess if there is no red flag on this implementation, I'll go ahead and finish the PR (the AS test suite is passing already, I don't think this PR will change a lot to have a green CI).

Regarding

Broadcasting is in a grey zone and not referenced in our docs but I saw it often in apps and libraries. This refactor would break them.

Should we go through a deprecation cycle or nop?

Copy link
Member

Choose a reason for hiding this comment

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

nop. The broadcast method is nodoced.

I have no reservations about this implementation. I think it will be way easier to use and maintain.

@Edouard-chin Edouard-chin force-pushed the ec-logger branch 2 times, most recently from 7fc578f to 77de8a6 Compare July 3, 2023 21:53
@Edouard-chin Edouard-chin changed the title Refactor tagged logging and add a public API for broadcasting logs Add a public API for broadcasting logs Jul 3, 2023
@Edouard-chin Edouard-chin force-pushed the ec-logger branch 2 times, most recently from 2f0d5cf to e3fc8f8 Compare July 3, 2023 22:11
@Edouard-chin
Copy link
Member Author

Originally wanted to fix TaggedLogging and implement a better broadcast logger at the same time (because they are kind of coupled and the issues occur with tagged logging when used with a broadcast), but the changes are too big for a single PR and the the many diff hunks would be annoying to review.

I changed this PR to focus only on broadcasting and will propose the change for tagged logging in another PR.

def progname; end

# Implemented for duck typing.
def level; 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.

Not sure what's the best approach here.

In the context of a broadcast, getting the level (and the progname above) don't make sense because the level on each loggers that are part of the broadcast can be different from one another.

In the previous implementation, level would return the level of the first logger in the broadcast. So I think we have a few options:

  1. Do the same as before and return the level of the first logger.
  2. Always return nil
  3. Don't implement the method.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe return the higher level between all the loggers that are going to be brodcasted to?

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 idea 👍

def progname; end

# Implemented for duck typing.
def level; end
Copy link
Member

Choose a reason for hiding this comment

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

Maybe return the higher level between all the loggers that are going to be brodcasted to?

end

# Implemented for duck typing.
def progname; end
Copy link
Member

Choose a reason for hiding this comment

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

Maybe return something like "Broadcast"?


# Add logger(s) to the broadcast.
#
# @param loggers [Logger] Loggers that will be part of this broadcast.
Copy link
Member

Choose a reason for hiding this comment

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

Rails doesn't use yard, so let's change the documentation to follow Rails conventions.

@formatter = formatter
end

def progname=(progname)
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to allow assigning the progname? I think all the loggers inside the broadcast will have different prog names and we don't want to make all of them the same.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I agree that assigning the progname for all loggers is weird. But I was thinking that the behaviour on this broadcast should make it clear that any methods called on it will modify all inner loggers. Could be confusing if progname= is the only exception to that rule?

No strong opinion though and I'm happy to change it.

Copy link
Member

Choose a reason for hiding this comment

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

Good point. But I think progname is really uniq for each logger so I think it would be better to now allow a mass update. We can even raise an exception. But given progname will return Boardcast make this method change only the progname of the broadcast?

@Edouard-chin Edouard-chin force-pushed the ec-logger branch 3 times, most recently from f2924c6 to cf65083 Compare August 14, 2023 14:32
@zzak zzak added this to the 7.1.0 milestone Sep 23, 2023
@zzak
Copy link
Member

zzak commented Sep 23, 2023

@Edouard-chin Could you squash your commits and rebase the changelog please? 🙏

@Edouard-chin
Copy link
Member Author

Edouard-chin commented Sep 24, 2023 via email

@Edouard-chin
Copy link
Member Author

Should be good for another 👀

- ## Context

  While working on rails#44695, I
  realised that Broadcasting was still a private API, although it’s
  commonly used. Rafael mentioned that making it public would require
  some refactor because of the original implementation which was hard
  to understand and maintain.

  ### Changing how broadcasting works:

  Broadcasting in a nutshell worked by “transforming” an existing
  logger into a broadcasted one.
  The logger would then be responsible to log and format its own
  messages as well as passing the message along to other logger it
  broadcasts to.

  The problem with this approach was the following:

  - Heavy use of metaprogramming.
  - Accessing the loggers in the broadcast wasn’t possible.
    Removing a logger from the broadcast either.
  - More importantly, modifying the main logger (the one that broadcasts
    logs to the others) wasn’t possible and the main source of
    misunderstanding.

    ```ruby
      logger = Logger.new(STDOUT)
      stderr_logger = Logger.new(STDER))
      logger.extend(AS::Logger.broadcast(stderr_logger))

      logger.level = DEBUG # This modifies the level on all other loggers
      logger.formatter = … # Modified the formatter on all other loggers
    ```

  To keep the contract unchanged on what Rails.logger returns, the new
  BroadcastLogger class implement duck typing with all methods
  that has the vanilla Ruby Logger class.

  It's a simple and boring PORO that keeps an array of all the loggers
  that are part of the broadcast and iterate over whenever a log is
  sent.
  Now, users can access all loggers inside the broadcast and modify
  them on the fly. They can also remove any logger from the broadcast
  at any time.

  ```ruby
  # Before

  stdout_logger = Logger.new(STDOUT)
  stderr_logger = Logger.new(STDER)
  file_logger = Logger.new(“development.log”)
  stdout_logger.extend(AS::Logger.broadcast(stderr_logger))
  stdout_logger.extend(AS::Logger.broadcast(file_logger))

  # After

  broadcast = BroadcastLogger.new(stdout_logger, stderr_logger, file_logger)
  ```

  I also think that now, it should be more clear for users that the
  broadcast sole job is to pass everything to the whole loggers in
  the broadcast. So there should be no surprise that all loggers in
  the broadcast get their level modified when they call
  `broadcast.level = DEBUG` .

  It’s also easier to wrap your head around more complex setup such
  as broadcasting logs to another broadcast:
  `broadcast.broadcast_to(stdout_logger, other_broadcast)`
@rafaelfranca rafaelfranca merged commit 4c72cc2 into rails:main Sep 25, 2023
4 checks passed
@miharekar
Copy link
Contributor

miharekar commented Sep 28, 2023

I'd like to write a proper issue/PR but I'm not sure how to debug further regarding logging changes.

Logs worked fine in 7.1.0.beta1 but in 7.1.0.rc1 (with this in) I no longer have most logs in stdout, only in development.log. I only have Started PATCH… and similar in stdout, but nothing about request params or activerecord, or anything. It is in development.log though.

I tried creating a fresh rails app with a generator, and the logs showing in stdout there, as expected. I've looked at app and environment configs and can't see any diff there. I've put a debugger inside LogSubscriber but it looks the same for both apps. So I'm not sure what's going on and how to debug it further 🤔

Here's the app: https://github.com/miharekar/decent-visualizer/tree/broken-logs

@Edouard-chin
Copy link
Member Author

Thanks for letting me know, you are right I'm seeing the same thing. Sorry for missing that. Looking now 👀

@Edouard-chin
Copy link
Member Author

Opened a PR in #49417 to fix this.

Edouard-chin added a commit to Edouard-chin/rails that referenced this pull request Sep 29, 2023
- An oversight of rails#48615 is that it changes the `Rails.logger` to be
  a broadcast logger after the app is booted. Anything referencing
  `Rails.logger` during the boot process will get a simple logger and
  ultimately resulting in logs not being broadcasted.

  For example `ActionController::Base.logger.info("abc")` would
  just output logs in the `development.log` file, not on STDOUT.

  ----

  The only solution I could think of is to create a BroadcastLogger
  earlier at boot, and add logger to that broadcast when needed (instead
  of modiyfing the `Rails.logger` variable).
st0012 added a commit to getsentry/sentry-ruby that referenced this pull request Oct 1, 2023
In rails/rails#48615, Rails 7.1 introduced a new
BroadcastLogger class that allows users to send logs to multiple loggers,
which means we need to adjust the SDK's logger assignment for it.
st0012 added a commit to getsentry/sentry-ruby that referenced this pull request Oct 1, 2023
In rails/rails#48615, Rails 7.1 introduced a new
BroadcastLogger class that allows users to send logs to multiple loggers,
which means we need to adjust the SDK's logger assignment for it.
st0012 added a commit to getsentry/sentry-ruby that referenced this pull request Oct 1, 2023
In rails/rails#48615, Rails 7.1 introduced a new
BroadcastLogger class that allows users to send logs to multiple loggers,
which means we need to adjust the SDK's logger assignment for it.
st0012 added a commit to getsentry/sentry-ruby that referenced this pull request Oct 2, 2023
* Adopt Rails 7.1's new BroadcastLogger

In rails/rails#48615, Rails 7.1 introduced a new
BroadcastLogger class that allows users to send logs to multiple loggers,
which means we need to adjust the SDK's logger assignment for it.

* Update changelog
elliterate added a commit to elliterate/saml_idp that referenced this pull request Oct 13, 2023
Rails 7.1.0 introduces a new default logger class,
ActiveSupport::BroadcastLogger, which does not inherit from Logger.
(See: rails/rails#48615.)

Instead of testing for a specific class, we can test the interface.
andrewn617 pushed a commit to Shopify/rails that referenced this pull request Oct 16, 2023
- An oversight of rails#48615 is that it changes the `Rails.logger` to be
  a broadcast logger after the app is booted. Anything referencing
  `Rails.logger` during the boot process will get a simple logger and
  ultimately resulting in logs not being broadcasted.

  For example `ActionController::Base.logger.info("abc")` would
  just output logs in the `development.log` file, not on STDOUT.

  ----

  The only solution I could think of is to create a BroadcastLogger
  earlier at boot, and add logger to that broadcast when needed (instead
  of modiyfing the `Rails.logger` variable).
Zogoo pushed a commit to saml-idp/saml_idp that referenced this pull request Jan 3, 2024
Rails 7.1.0 introduces a new default logger class,
ActiveSupport::BroadcastLogger, which does not inherit from Logger.
(See: rails/rails#48615.)

Instead of testing for a specific class, we can test the interface.
Zogoo pushed a commit to saml-idp/saml_idp that referenced this pull request Jan 3, 2024
Rails 7.1.0 introduces a new default logger class,
ActiveSupport::BroadcastLogger, which does not inherit from Logger.
(See: rails/rails#48615.)

Instead of testing for a specific class, we can test the interface.
jphenow pushed a commit to saml-idp/saml_idp that referenced this pull request Jan 3, 2024
Rails 7.1.0 introduces a new default logger class,
ActiveSupport::BroadcastLogger, which does not inherit from Logger.
(See: rails/rails#48615.)

Instead of testing for a specific class, we can test the interface.

Co-authored-by: zogoo <ch.tsogbadrakh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants