-
Notifications
You must be signed in to change notification settings - Fork 2
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
refactor: Redesign main modules to enable ad-hoc usage #48
Conversation
f92c735
to
97aab0f
Compare
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.
👋 A lot of changes so I am not sure I saw everything.
I'am not a fan of breaking the discord context and spreading logic around (Storage, Mix task ...)
I prefer to have all the api calls in API even if they are not all used in production like the mix tasks. From my experience it's easier to maintain / extend the api this way.
For the API call that require a bit of twist or glue like fetch_or_create, maybe_*, get_occurrence_threads I prefer them to live in a separate module Context
like before or to be grouped at least cause they are quite complicated and they should expose the most simple function to the consumer like {:ok, data}
{:error, reason}
without having to know how it works.
Then should they be a Sub module of Discord
is a choice to make.
"message" => %{ | ||
"content" => _ | ||
}, | ||
"name" => <<_::binary-size(16)>> <> " Elixir.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.
The title aka name where not tested. It's a good idea to test it adding a public helper that use a internal module would be nicer if we happen to change the fingerprint.
"message" => %{ | ||
"content" => _ | ||
}, | ||
"name" => <<_::binary-size(16)>> <> " Elixir.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.
The title aka name where not tested. It's a good idea to test it adding a public helper that use a internal module would be nicer if we happen to change the fingerprint.
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'm a bit confused about those changes.
This test are important because it's all the case that detach the logger handler
Also the return and how it's formatted after passing by the handler is not tested anymore.
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.
The idea was to only test integration here, and leave detailed assertions of error shape to tests in other modules, but in retrospect, that doesn't seem like too much hassle. I added the assertions back!
Sure, we can move them back into a single place. You have any thoughts on the name? I'd argue that having two
One problem I usually run into with modules like that is that they inevitably swallow errors when they return error tuple. The caller is usually the one who knows what to do in case of an error, and losing a detailed error can make it harder to handle different scenarios or uncover bugs. There's a huge difference between |
While I've also encountered issues with swallowed errors, for the Discord integration specifically, I see it this way:
I don't want the caller to decide what's a success and what is an error and what error should be handled. I prefere those to be handled in the called module and the api call will probably have shared logic like 400 case. with {:ok, %{status: 201, body: %{"id" => thread_id}}} <-
API.post_thread(config.discord_client, config.occurrences_channel_id, message) do
Storage.add_thread_id(config.supervisor_name, error.fingerprint, thread_id)
# added stuff for illustration purpose
else
{:error, %Req.SomeNetworkError} ->
# transient
%{:ok, %{status: 401, body: _} ->
# throw or do something
other_ ->
swallow ?
end
I agree no log from a library unless configured. I noticed you removed the configurable logger for discord could you bring it back it's very usefull for dev. |> maybe_add_debug_log(config.enable_discord_log)
end
defp maybe_add_debug_log(request, false), do: request
defp maybe_add_debug_log(request, true) do
Req.Request.append_response_steps(request, log_response: &log_response/1)
end
defp log_response({req, res} = result) do
Logger.debug("""
Request: #{inspect(to_string(req.url), pretty: true)}
Request: #{inspect(to_string(req.body), pretty: true)}
Status: #{inspect(res.status, pretty: true)}
Body: #{inspect(res.body, pretty: true)}
""")
result
end |
I pushed a commit where I created a |
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 like the direction where it's going 🚀
I notice a small issue but it's still wip.
lib/mix/tasks/disco_log.create.ex
Outdated
Discord.fetch_or_create_channel( | ||
config, | ||
with {:ok, channels} <- Discord.list_channels(config.discord_client, config.guild_id), | ||
{:ok, %{status: 201, body: %{"id" => category_id}}} <- |
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.
If one channel already exist the pattern matching will fail.
fetch_or_create_* should return {:ok, channel}
Low chance to happen because they will be all created or none most of the time.
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.
Ah, right, fixed that!
lib/disco_log/discord.ex
Outdated
@impl true | ||
defdelegate create_occurrence_message(config, thread_id, error), to: Discord.Context | ||
def create_channel(discord_client, guild_id, channel) do | ||
API.create_channel(discord_client, guild_id, channel) |
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.
A successful creation should return a 201 status and a channel and should be unwrapped to {:ok, channel}
They can be handled by the called module, sure, but ultimately only the caller can decide what to do. Take 400 error for example. If Then there's the topic of error message. In two scenarios above, we want to ideally log as much information as possible and in many cases, HTTP response is the best thing available. Enabling a debug mode with full request logging is definitely possible and good, but you will have to hope that the same error will happen again and that it is indeed the same error. Not to mention debug logs tend to be noisy and hunting down errors can be tricky. If you hide 401 behind It's just hard to see the good side of a wrapper module I think? Maintainability usually means that we can write something once an reuse in other places, but every single function in Anyways, I won't insist on doing it my way, just wanted to ensure all arguments are laid out clearly. |
I get your point about error granularity and letting the caller decide how to handle things like 400 errors. But I still think the context module has value for what we consider a successful call. For example, it helps keep For functions where we want access to the error body, what do you think about this pattern and letting the raw error pass through? with {status: 201, body: %{"stuff" => stuff}} <- Api.create_stuff(attrs) do
map_stuff(stuff)
end The API calls that don’t have data mapping don’t need to be in the wrapper/context. |
Wouldn't it be more like this with some errors being also wrapped in :ok tuple like with {:ok, %{status: 201, body: %{"stuff" => stuff}}} <- Api.create_stuff(attrs) do
{:ok, map_stuff(stuff)}
end that would probably need to be something like case API.create_stuff(stuff) do
{:ok, %{status: 201, body: %{"stuff" => stuff}}} -> {:ok, map_stuff(stuff)}
{:ok, resp} -> {:error, resp}
error -> error
end I think this is how it would naturally evolve once |
Y I like the idea of letter the raw error pass while mapping the success call if needed |
3ce5ae6
to
27ffac2
Compare
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.
👋 Looks good to merge
565da68
to
d6f33e0
Compare
Added back debug logs and squashed commits! |
d6f33e0
to
7a46d45
Compare
7a46d45
to
b4d7df7
Compare
🚀 |
Contributor checklist
For example:
fix: Multiply by appropriate coefficient
, orfeat(Calculator): Correctly preserve history
Any explanation or long form information in your commit message should be
in a separate paragraph, separated by a blank line from the primary message
This one is big conceptually, but in terms of lines count, it is a net negative. About 25% of added lines are canned API responses (more on this below).
Goal
My original goal here was to enable an ad-hoc usage of
DiscoLog
. Suppose you don't useDiscoLog
as a logger handler, but still want to manually publish messages to certain channels. This makes sense because a medium-sized application can generate too many logs to actually post into discord. But posting some logs (for example, a message that a new user signed up to a paid plan) into some channels is useful.In this scenario, you're likely to not have a valid config on your hands and virtually all parts of DiscoLog required it. In fact, there were two types of configs:
DiscoLog.Config
andDiscoLog.Discord.Config
.Hopefully, this refactoring will make things simpler.
Changes
DiscoLog.Discord
module was removedThe problem with
Discord
module was that it served as a watershed between two parts of the system, both of which were important. However, becauseDiscord
was used for mocking, the logic behind it was untested.Win: the tests now able to reach behind
Discord
module all the way to HTTP requests and test more.DiscoLog.Discord.Context
andDiscoLog.Discord.Client
were dissolvedDiscord.Context
andDiscord.Client
did many things: they served as modules making HTTP requests for other modules as well as preparing messages and errors for submitting to the API.Having 2 pair of modules named
Context
andClient
is a little confusing, so I split their responsibilities around. Mainly betweenDiscoLog.Discord.API
andDiscoLog.Prepare
New module
DiscoLog.Prepare
The logic about how to represent errors and log messages as discord messages lives here. This module is akin to what is usually called Formatters, but I chose a different name here to avoid confusion with logger handler formatters. This module consists of only pure functions which makes it convenient to test.
New module
DiscoLog.Discord.API
This is a Discord API client inspired by Req's take on SDKs. It is a behaviour, but instead of defining every endpoint as a callback, it defines only 2:
client/1
andrequest/4
. This should be more than enough to allow anyone who would want to switch to another HTTP client to do so.Besides callbacks ,
DiscoLog.Discord.API
also includes functions for API calls used by the application, such aslist_active_threads
andpost_thread
. Note that endpoints only used by mix commands are not included as functions. Instead, mix commands issue ad-hoc requests.API modules come with default implementation
API.Client
. For tests, there is alsoAPI.Mock
andAPI.Stub
. Stub modules contains a collection of response examples that will be returned by default. In my experience, having canned responses is very convenient for development.Win: This change makes HTTP client fully replaceable. In fact, we can make
Req
dependency optional now. This also allows users to call discord endpoints not used byDiscoLog
, e.g.:Changes to
DiscoLog.Config
before_send
is removed. I don't think it was used, and I think the equivalent functionality can be achieved via logger filters.discord
option was changed todiscord_client_module
and is nowDiscoLog.Discord.API.Client
by default.During validation,
Config
will now calldiscord_client_module.client(token)
and save it asdiscord_client
for everyone to use.DiscoLog.Client
is merged intoDiscoLog
DiscoLog.Client
is an interesting one. It had a number of fairly genericsend_error
,log_info
andlog_error
functions, which for me look very similar toDiscoLog.report
, so I moved them toDiscoLog
DiscoLog.Discord.Config
is removedAfter removing the watershed which
Discord
was, there is no need in discord config anymore.Changes to
DiscoLog.LoggerHandler
I removed
adding_handler
andchanging_config
callbacks. They are used for fairly advanced use cases, which are probably beyond DiscoLog intended applications.Result
There are undoubtedly some rough edges worth further investigating and work, but I'm happy to report that ad-hoc usage is unblocked. Here's how I can post a message to arbitrary channel with just token and channel_id: