I recently did a live coding session on a 3d audio server built in Elixir.
This project involves quite a complicated supervision tree where GenServers are created and destroyed dynamically based on users joining a room and moving around.
As such this requires in-depth logging of processes in the application.
[VollumaServer]
|
-------------
| |
[ROOMSERVER:1] [ROOMSERVER:2]
| |
--------------------- ------------------
| | | |
[MEMBERSHIPS:1] [CELLS:1] [MEMBERSHIPS:2] [CELLS:2]
| | | | | | | | | | | | | | | | | | | | |
U U U U U U U U 1 2 3 4 U U U U U U 1 2 3
Regulary I found myself logging PIDs (process IDs) as I dynamically created GenServers.
The code would look something like this:
def start_link(room = %Room{}) do
{:ok, pid} = DynamicSupervisor.start_link(
__MODULE__, room, name: registry_name(room)
)
Logger.debug fn ->
"Created CellsSupervisor for room #{room.id} at #{inspect pid}."
end
{:ok, pid}
end
The line I’d like to focus on is this one:
"Created CellsSupervisor for room #{room.id} at #{inspect pid}."
Notice how use the room.id
without any processing? The pid
however
must be inspect
ed.
The reason for this is that number and string types (along with most basic types) in Elixir implement the String.Chars protocol.
For those new to Elixir, protocols are the language’s way of implementing a sort of polymorphism without breaking Elixir’s fabled immutability. They are really fascinating and one of Elixir’s most underrated features.
The String.Chars
protocol is the protocol that defines how an Elixir
type is converted to a string.
It seems a little bit of an oversight perhaps, considering how
essential the PID
type is in Elixir development, to not have a native
implementation of the String.Chars
protocol as part of the language.
Trying to interpolate a String
with a bare PID
will result in the
dreaded error:
iex> IO.puts "This will cause the error: #{self()}"
** (Protocol.UndefinedError) protocol String.Chars
not implemented for #PID<0.100.0>
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:22: String.Chars.to_string/1
Thankfully, we can easily get around this for any type in Elixir by calling the
Kernel.inspect
method on it (or just use inspect
for short).
iex> IO.puts "Yay! This works: #{inspect self()}"
Yay! This works: #PID<0.100.0>
However, there is a better way.
#PID<0.100.0>
is an ugly thing to have strewn about our logs, and
gives us very little information about what the process behind the PID
is doing.
Let’s read that error again.
protocol String.Chars not implemented for #PID<0.100.0>
Let’s implement the String.Chars
protocol so that we can use a PID
in our logs without having to inspect
it, and possibly add some useful
information alongside.
Disclaimer: This may have a performance impact on your application if you’re not careful. I’d suggest only using PIDs in debug logs, and to also ensure all calls to Logger.debug and its siblings are given an anonymous function instead of interpolated strings. This allows the Erlang VM to omit expensive iterpolations,
Here is my inital implementation of the String.Chars
protocol:
# ./lib/utils/pid.ex
defimpl String.Chars, for: PID do
def to_string(pid) do
info = Process.info pid
name = info[:registered_name]
"#{name}-#{inspect pid}"
end
end
Here we retrieve the “registered name” of the module (what you supply as
the name
when you call start_link
on a GenServer
).
You can retrieve additional information as well here if it suits your needs.
Personally, I have found these few lines of code invaluable in debugging my complicated Elixir supervision trees and logging the connections between different processes in an application.
Hopefully this helps out fellow Elixir enthusiasts in their travels! Take it easy. :)