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

lazily render debug logs #438

Merged
merged 2 commits into from
Nov 29, 2018
Merged

Conversation

bgentry
Copy link
Contributor

@bgentry bgentry commented Oct 31, 2018

In our app, we had some emails that were spending a massive amount of time sending, about 50-60 seconds after the email contents had already been generated. This corresponded with crazy memory and CPU usage.

As a disclaimer, our app has some bad issues with excessive preloads that we are working on fixing. But where that data loading takes a few seconds, sending the email itself was taking an additional minute. It took me some digging to find out that the delay was happening between the call to our mailer and the adapter.

Without this change, inspect() is called for every email, regardless of the log level. After this change, inspect() only gets called if the log level is :debug.

Two additional concerns I wanted to discuss:

  1. The reason this is such an issue is because :assigns are passed along as part of the %Email{} even after rendering has completed. This is why inspect() is so expensive, because we have a lot of nesting of data structs that are being inspected and logged. Is there any reason that deliver_now() and deliver_later() couldn't strip the :assigns from the Email before doing anything extra with them? Since the email has already rendered at this point, it doesn't seem like they serve much purpose.
  2. Do we need to have the hardcoded limit: :infinity as part of the debug call? That turns this into a potentially severe issue, whereas it normally might not matter much if just part of a deeply nested dataset is logged.

Without this, `inspect()` is *always* called for every email, regardless
of the log level. After this change, `inspect()` only gets called if the
log level is debug or more verbose.

This can potentially save a massive amount of time depending on the size
of the Email struct (and whatever `:assigns`) it has.
@shamshirz
Copy link

👍 We had the same issue, caused by inspecting large attachments.
@bgentry You can try setting compile_time_purge_level: :debug in your :logger config for production .

@bgentry
Copy link
Contributor Author

bgentry commented Nov 28, 2018

@paulcsmith friendly ping on this PR 👋 Also interested to hear your thoughts on my other questions so I can help fix those too.

Copy link
Contributor

@paulcsmith paulcsmith left a comment

Choose a reason for hiding this comment

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

One comment and one question. Thanks for the friendly ping and thanks for being so patient!


#{inspect(email, limit: :infinity)}
""")
#{inspect(email, limit: :infinity)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Good point in the PR description. I think :infinity can be removed 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are a handful of inspect(email, limit: :infinity) calls in other parts of the library too. Would you prefer to just remove that option entirely, or to set some other value higher than the default of 50?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think something higher than 50, but not sure what. Since it is kind of arbitrary, maybe 150? Sounds like a decent in-between. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That seems fine. I changed all the :infinity opts to 150 now ✌️

@@ -141,19 +141,23 @@ defmodule Bamboo.Mailer do
end

defp debug_sent(email, adapter) do
Logger.debug("""
Sending email with #{inspect(adapter)}:
Logger.debug(fn ->
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 not sure I understand this. If this is a function, does that mean something else needs to call this? Or does Logger.debug accepts a function it will render the debug logs async?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Logger.debug (or any similar logger function) accepts a function to be dynamically called if the log level is in scope: https://hexdocs.pm/logger/Logger.html#debug/2

Logger.debug "hello?"
Logger.debug fn -> "dynamically calculated debug" end
Logger.debug fn -> {"dynamically calculated debug", [additional: :metadata]} end

Copy link
Contributor

Choose a reason for hiding this comment

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

Very cool! TIL :)

@paulcsmith
Copy link
Contributor

Oh and about assigns, I think it can be valuable to include because sometimes an assign is missing or not what is expected

Using infinity can lead to some extremely slow logging when large
structs are being passed in. See beam-community#438 for more details.
Copy link
Contributor

@paulcsmith paulcsmith left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you :)

@paulcsmith paulcsmith merged commit 23b8dc8 into beam-community:master Nov 29, 2018
@bgentry bgentry deleted the lazy-debug-logs branch November 30, 2018 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants