BEAM Process Labels

elixir
Posted on: 2024-03-12

A nice improvement for observability is coming in Erlang/OTP 27.0 and therefore to Elixir also. And while I didn't implement it in Erlang - I don't have enough expertise for that - I did see the need for it and pester people ask for it, which may have helped get the ball rolling.

The TL;DR is:

  • A process can set a label for itself, which does not have to be unique. This may be useful for identifying the role that it plays, like "a database connection", or "a chat with user 5".
  • Any other process can get that label; it will show up in tools like Observer and will be included in crash logs starting in Erlang/OTP 27.0 and Elixir 1.17, making your application easier to understand.

Details

On May 6, 2021, I wrote to the since-retired Erlang mailing list with a question.

Is there a way to add a non-unique label to a process? If not, I would like to propose that it be added.

At that time, you could basically see two types of processes in a tool like Observer:

  • Ones with unique registered names
  • Completely anonymous ones, identified only by PID

What I wanted was a middle ground: processes that have some role we could name, like "a web request handler" or "a background job", but not a unique name.

For example, "a database connection" in the db_connection library is created using the DBConnection.Connection module, but you will always have multiple of these. In a process tree, you can probably guess what they are by their position (they are the ones on the right here):

DBConnection tree with processes on the right unlabeled

...but it would be nicer if they were labeled:

DBConnection tree with processes on the right labeled like "pool <0.461.0>" or "connection <0.467.0>"

As another example, in your application, you might want to label a LiveView with something like {:live_chat, "foo@example.com"}. The label might not be unique - that user could have multiple browsers open - but it would still be useful.

These kinds of labels are now possible! After much discussion on the email thread and a valiant attempt at an implementation by Roger Lipscombe, the feature was finally implemented by Dan Gudmundsson and is part of Erlang/OTP 27.0-rc1 (see also the Erlang Forum announcement). The second image above is a real screenshot from Observer, using OTP 27.0-rc1 and with my local copy of :db_connection, modified to add labels using :proc_lib.set_label/1.

With guidance from the Elixir Core team, I landed a PR to add Process.set_label/1 to Elixir for convenience, temporarily using Process.put/2 directly until Elixir depends on Erlang/OTP 27+. It will be available starting in Elixir 1.17.0.

A corresponding Process.get_label/1 will have to wait for an Elixir version that requires Erlang/OTP 27, or at least Erlang/OTP 26.2. Technically, on older versions of Erlang/OTP, one process could examine the process dictionary of another using Process.info(pid, :dictionary), then get the label from there. But this is a debugging technique and not recommended for production. :proc_lib.get_label/2 in Erlang/OTP 27.0 uses a feature added in Erlang/OTP 26.2 for getting a value from the dictionary of another process without retrieving its entire dictionary, like this:

{{:dictionary, :some_key}, val} =
  :erlang.process_info(some_pid, {:dictionary, :some_key})

In the meantime, you could define either or both of these label functions yourself; for example, if you want to get labels to display in your observability library but don't want to blow up for users who aren't yet using Erlang/OTP 27.

defmodule MyApp.ProcessLabels do
  def set_label(label) do
    if function_exported?(:proc_lib, :set_label, 1) do
      # Avoid a compiler warning if the function isn't
      # defined in your version of Erlang/OTP
      apply(:proc_lib, :set_label, [label])
    else
      # This entry in the process dictionary happens
      # to be where the label goes (at least for now)
      Process.put(:"$process_label", label)
      # mimic return value of `:proc_lib.set_label/1`
      :ok
    end
  end

  def get_label(pid) do
    if function_exported?(:proc_lib, :get_label, 1) do
      # Avoid a compiler warning if the function isn't
      # defined in your version of Erlang/OTP
      apply(:proc_lib, :get_label, [pid])
    else
      # mimic return value of
      # `:proc_lib.get_label/1` when none is set.
      # Don't resort to using `Process.info(pid, :dictionary)`,
      # as this is not efficient.
      :undefined
    end
  end
end

In the future, I hope that more tools and error messages can give us more context about what processes we're dealing with.

My requests to Elixir and Erlang developers:

  • As soon as it's feasible, start labeling your currently-anonymous processes, especially if you are a library author
  • If you build observability tools, display these labels when they're available
  • Keep thinking of ways to make our systems more understandable