The :sys
module in OTP actually provides a really good example of a good way to do this.
If you look at the :gen_statem
loop it has the Debug
parameter. Thatās the metadata for the sys_debug
function.
If Debug
is []
returns []
, in other words itās a noop
, otherwise it invokes :sys.handle_debug
with the Debug
metadata.
Since, the :sys
module has a ton of different methods to log/debug I wonāt go into what the meta data contains.But, if you used :sys.trace/2
it basically invokes the print_event
function with the IO of stdout
being the Dev
argument.
Itās been a couple of months so I might be a bit rusty but, a working example may be:
defmodule Stuff do
use GenServer
def start_link(args, opts \\ []) do
{trace, opts} = Keyword.pop(opts, :trace, [])
{log, opts} = Keyword.pop(opts, :log, [])
GenServer.start_link(__MODULE__, {args, trace ++ log}, opts)
end
def trace(pid, flag) do
GenServer.call(pid, {:trace, flag})
end
def log(pid, flag) do
GenServer.call(pid, {:log, flag})
end
def init({state, log_meta}) do
log_fun(log_meta, state, :init)
{:ok, {state, log_meta}}
end
def handle_call({:trace, true}, {pid, _}, {state, log_meta}) do
log_fun(log_meta, state, {:trace_on, pid})
{:reply, :ok, {state, [{:trace, true} | log_meta]}}
end
def handle_call({:trace, false}, {pid, _}, {state, log_meta}) do
log_fun(log_meta, state, {:trace_off, pid})
{:reply, :ok, {state, Keyword.delete(log_meta, :trace)}
end
def handle_call({:log, true}, {pid, _}, {state, log_meta}) do
log_fun(log_meta, state, {:log_on, pid})
{:reply, :ok, {state, [{:log, true} | log_meta]}}
end
def handle_call({:log, false}, {pid, _}, {state, log_meta}) do
log_fun(log_meta, state, {:log_off, pid})
{:reply, :ok, {state, Keyword.delete(log_meta, :log)}
end
defp log_fun([], _, _), do: :noop
defp log_fun([{:trace, true} | log_meta], state, event) do
IO.puts format_event(state, event)
log_fun(log_meta, state, event)
end
defp log_fun([{:log, true} | log_meta], state, event) do
Logger.info format_event(state, event)
log_fun(log_meta, state, event)
end
defp format_event(state, :init) do
"Initializing with #{inspect state}"
end
defp format_event(_, {:trace_on, pid}) do
"Tracing turned on by #{inspect pid}"
end
defp format_event(_, {:trace_off, pid}) do
"Tracing turned off by #{inspect pid}"
end
defp format_event(_, {:log_on, pid}) do
"Logging turned on by #{inspect pid}"
end
defp format_event(_, {:log_off, pid}) do
"Logging turned off by #{inspect pid}"
end
end
Itās a lot of work for a pretty basic example, but once you have this much adding new events or even new logging methods is super simple.
Plus, if you wanted you could forego the entire format_event
function. I just think that it cleans it up a bit and defers the binary creation until it actually needs to be written.