Erlang's dbg module provides the ability to trace functions, processes, and messages from within the terminal. I'd not played with it before tonight - let's see what all it can do.


So we're just going to do all of this from within an iex session. Fire up iex.

Debugger help

The dbg module ships with its own tiny help feature. Let's look at that briefly:



The following help items are available:
   p, c
       - Set trace flags for processes
   tp, tpl, ctp, ctpl, ctpg, ltp, dtp, wtp, rtp
       - Manipulate trace patterns for functions
   n, cn, ln
       - Add/remove traced nodes.
   tracer, trace_port, trace_client, get_tracer, stop, stop_clear
       - Manipulate tracer process/port
       - Info

call dbg:h(Item) for brief help a brief description
of one of the items above.

Alright, so here are the things you can do with this module. Let's look at the help for :tracer:



iex(25)> :dbg.h(:tracer)
tracer() -> {ok, pid()} | {error, already_started}
 - Starts a tracer server that handles trace messages.
tracer(Type, Data) -> {ok, pid()} | {error, Error}
 - Starts a tracer server with additional parameters

Alright, I won't go through all of these, but it's neat enough that this help facility is built in. On to the debugging.

Basic Tracing

Let's start off by defining a module:

defmodule Foo do
  def bar do
    IO.puts "baz"

Alright, so this is about as simple of a situation as we could have - this is just a function that calls a function on the IO module. Let's trace it to see what happens when we call it.

:dbg.c(:"Elixir.Foo", :bar, []) # do not hit enter yet.

Alright, so here we're calling the c/3 function on the :dbg module. This is a native erlang module, which is why we reference it as its atom. It takes a traditional MFA-style triplet of arguments. The first argument is the atom representing our Foo module - since we're calling it from erlang code, we have to provide its fully qualified name. If you weren't already aware, elixir modules are just their module names, prefixed with "Elixir.", as an atom.

Anyway, this will call the function, and provide the trace. Go ahead and run it. (hit enter)

Here, you can see all of the functions that were called and messages that were passed in order to fulfill this function invocation - it's a lot of detailed information that you just can't get without instrumentation in a lot of other languages. This is surprising to me, and it's pretty cool.

Setting trace flags on a pid

Alright, so say you wanted to trace just one particular pid - for instance, any messages or process events that occurred for that pid. You can do this too. First, we'll spawn a process:

defmodule Echo do
  def start do
    receive do
      {from, message} ->
        send(from, String.upcase(message))
pid = spawn(Echo, :start, [])

Next, we'll debug any messages or process events on that pid:

:dbg.p(pid, [:m, :procs])

Finally, we'll send it a message:

send(pid, {self, "hai"})


iex(22)> send(pid, {self, "hai"})
{#PID<0.42.0>, "hai"}
(<0.105.0>) << {<0.42.0>,<<"hai">>}
(<0.105.0>) <0.42.0> ! <<"HAI">>
(<0.105.0>) exit normal
iex(23)> flush

So here you can see that a message was sent to the pid we just spawned (pid 105) with the contents of our message (our pid, then the binary "hai"). You can see that it proceeded to send the upcased "HAI" binary back to us, and then exited normally. Finally, you can see that we received the message, if you flush this shell process's messages:


Pretty cool! What else can we do?

Find out what's being traced

So say you wanted to know what was being traced at present. You would do this:


It's showing no traces set up, because the pid that we were tracing exited already. Let's set up a trace for a little echo server again:

pid = spawn(Echo, :start, [])
:dbg.p(pid, [:m, :procs])

Alright, now run :dbg.i again. Now you can see the details on the current set traces.


Looking through the docs, there's a lot more that the dbg module can do, but this gets you a good feel for it without getting too boring and repetitive. See you soon!