The case of the Red Bug

It was yet another gloomy evening in the Beamopolis. Boxes hiding artifacts of solved and unsolved cases cluttered my room. Dim light passing through my small window covers everything. I was sitting in front of my desk and was oscillating between the dream world and reality. A brutal phone call woke me up, and my intuition kicked in, telling me this will not be another typical case of a messy exception that went wrong.

I was right; this time, my caller was asking impossible. She wanted me to find a way to trace a suspect. Still, not only that, but the requirement was also to see all the breadcrumbs left by a suspect. She also demanded to gather all information about suspect inputs and outputs – all that with a possibility to have insights on-demand, remotely without even meeting me in person. My initial gut feeling told me to hang up the phone right away, but as always, with those cases, there is an urge to stand up to the challenge and try to accomplish the impossible.

For better or worse, I've agreed. Now I was standing next to my window, looking at all those actors passing by, not aware of each other minds but beautifully arranged in asynchronous dance on message lanes. I had to find a way to trace my suspect in the worst part of Beamopolis – Complexity District 9. The story tells that no one knows what happens behind the defmodule lines separating the district from the rest of the city – yet everyone communicates with them daily. I was skimming through my options.

A Tracer Trevor owns me a favor from our last case, but he was not ideal for the task at hand. It was hard to make him not put all his information to standard output but rather send it to me via a message. I also thought about the famous hacker going with the nickname dbg, but she was quite rusty and might not have been able to address all my requirements. Then I've had an a-ha moment; there is already an excellent tracer in the city – redbug. He does not speak elixirish; instead, he is fluent in erlangish – if there only be a way to find a decent translator. After a few phone calls, I was lucky enough to find a fine young lady – rexbug, who agreed to help me out and translate what redbug was saying to the language I speak.

Unfortunately for me, redbug had a similar limitation (at first glance only as I was going to find out) that it was outputting his findings to the standard output of his. The approach would not work for me as my mysterious caller demanded to have a possibility to query traces 24/7. That meant I have to build a specific endpoint for her, and tracing my suspect should be automated.

After going back and forth with rexbug, we've found a way. She told me that I could use a custom function to print whatever redbug found to wherever I want to. I knew that was it. At this stage, I knew I would be able to finish what I've started. I've come up with the following invocation.

TraceClient.start(
  [
    "Main.Suspect :: return"
  ],
  time: @timeout,
  msgs: @max_number_of_messages,
  print_fun: fn entry -> GenServer.call(Tracer, {:trace_entry, entry}) end
)

Both @timeout and @max_number_of_messages were set up in agreement with rexbug that we won't try to trace my suspect for too long (or too short), and if he becomes too noisy, we would also cease the operation. After all, we do not want to go too deep inside the Complexity District 9.

With my invocation ready, I could do my tedious but so needed work to handle all the pieces of evidence and present them to my employer. I came up with the following.

defmodule Tracer do
  use GenServer

  alias Rexbug, as: TraceClient

  @timeout 2_000
  @max_number_of_messages 100_000

  # Client
  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def trace do
    GenServer.call(Tracer, {:trace})
  end

  def get_results do
    GenServer.call(Tracer, :get_results)
  end

  # Server (callbacks)
  @impl true
  def init(:ok) do
    {:ok, []}
  end

  @impl true
  def handle_call({:trace}, _from, state) do
    TraceClient.start(
      [
        "Main.Suspect :: return"
      ],
      time: @timeout,
      msgs: @max_number_of_messages,
      print_fun: fn entry -> GenServer.call(Tracer, {:trace_entry, entry}) end
    )

    try do
      Main.Suspect.call()
    rescue
      _ -> nil
    end

    TraceClient.stop()

    {:reply, :ok, state}
  end

  @impl true
  def handle_call({:trace_entry, trace_entry}, _from, state) do
    {:reply, trace_entry, state ++ [trace_entry]}
  end

  @impl true
  def handle_call(:get_results, _from, state) do
    result =
      Enum.map(state, fn entry ->
        {action, {{module, function, args}, retn}, {_, _}, {_, _, _, _}} = entry

        %{
          type: action,
          module: module,
          function: function,
          arguments: inspect(args),
          return_value: inspect(retn)
        }
      end)

    {:reply, {:ok, result}, []}
  end
end

With that good old GenServer approach, I can now trace my suspect whenever I want, and all traces gathered with my friends – redbug and rexbug are passed to my GenServer state, waiting there until I fetch them.

It is easy to use my work simply as

Tracer.trace()

and after traces are gathered fetch them as

case Tracer.get_results() do
  {:ok, results} -> results
  {:error, error} -> error
end

With all that, I get results in a nice form where I can see all suspect's interactions – what they received, and what they gave in return

{:ok,
 [
  %{
	 arguments: "we have to stash that gold somewhere",
	 function: :heist,
	 module: Main.Suspect,
	 return_value: "\"\"",
	 type: :call
  },

  ...

  %{
	 arguments: "1",
	 function: :heist,
	 module: Main.Suspect,
	 return_value: "{:error, \"uh! oh! they\'ve got me\"}",
	 type: :retn
  }
]}

Everything was there, right in front of me, now I could trace all naughty functions visited by my suspect, I knew what he passed to them and what they gave in return. That knowledge was exciting and scary at the same time. There was nothing he can do to hide anymore; even if he hides in the most complex part of the city, I will be there, always watching, always knowing.

I'm closing this case. The lady who started it decided to use this knowledge for good and created an excellent document where everyone could check what our suspect does in the city. It was a game-changer as no villain could hide anymore from good citizens of Beamopolis. Everyone was cheerful – until one day – I've picked up another phone call …