What You Need to Know About Logging in Elixir with Logger

If you have bug or you app is not working as expected, your log files may be critical to fixing the issue. We’ve covered the basics of debugging and now we’ll discuss what you need know about logging in your Elixir Phoenix application with Logger.

Logging keeps a record of data inputs, processes, data outputs, and final results in a program. You can traverse through logs and find out exactly where the application is having problems. Poor logs will leave you searching for answers, while a helpful log can assist you in resolving the bug right away.

Overview

We’ll cover the basics of Elixir Logger and use an example application to learn about different configuration and use cases.

  1. Logging in Elixir with Logger
  2. Phoenix Logging Example
  3. Demo Phoenix App
  4. Logging Levels
  5. Logging Metadata

Prerequisites

To follow this guide, make sure you have at least the following applications installed on your machine. In the brackets are the versions with which this guide was written.

  • Elixir (1.12.3-otp-24)
  • Erlang (OTP 24.1)
  • Phoenix (1.6.2)
  • Node.js with npm (15.14.0)

Logging in Elixir with Logger

Elixir programmers will often use IO.inspect/2 (guilty!) in their code as a quick and convenient debugging tool. Elixir provides Logger for logging in Elixir applications.

Using Elixir Logger is only a little more effort than that, but it’s much more elegant and flexible. As well as being useful for debugging, logging can also provide you with more – and better structured – information about the state and health of your application.

Logger provides debug, info, warn, and error levels. It supports multiple backends which are automatically supervised when plugged into Logger . Furthermore, it formats and truncates messages on the client to avoid clogging Logger backends. It also alternates between sync and async modes to remain performant when required but also apply backpressure when under stress. Finally, it integrates with Erlang’s :logger to convert terms to Elixir syntax.

We’ll use a sample project to learn more about Logger’s features.

Phoenix Logging Example

Before we get to the sample project, we’re going to look at how logging in implemented in Phoenix. When you start your server, the output in your console comes from Logger. Let’s check out the Phoenix.Logger module on GitHub.

Check out the code starting on line 205.

  def phoenix_endpoint_start(_, _, %{conn: conn} = metadata, _) do
    case log_level(metadata[:options][:log], conn) do
      false ->
        :ok

      level ->
        Logger.log(level, fn ->
          %{method: method, request_path: request_path} = conn
          [method, ?\s, request_path]
        end)
    end
  end

You can see if takes information from the conn to include in the log message when the endpoint starts. You may not understand how the entire module works, but hopefully this helps you make the connection between output in your console and code in libraries you are using.

Demo Phoenix App

Now let’s create a simple application, so we can implement logging for ourselves. Use the command line to update your Phoenix version and create a new Phoenix application.

$ mix archive.install hex phx_new 1.6.2
$ mix phx.new hello_logging

Change into the project directory, create your database and start your server.

$ cd hello_logging
$ mix ecto.create
$ mix phx.server

Now visit http://localhost:4000 and you should see the Welcome to Phoenix! page.

phoenix homepage

Check your console and you should see something like:

[info] GET /
[debug] Processing with HelloLoggingWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 30ms

You should have an idea now about where that logging info comes from in the Phoenix project code.

Logger Levels

Let’s play around with Logger levels. The supported levels are ordered by importance. You can update the config in your project to limit output to only certain levels.

The supported levels, ordered by importance, are:

  • :emergency – when system is unusable, panics
  • :alert – for alerts, actions that must be taken immediately, ex. corrupted database
  • :critical – for critical conditions
  • :error – error message
  • :warning – warning message
  • :notice – for normal, but significant, messages
  • :info – for information of any kind
  • :debug – debug message

For example, :info log level takes precedence over :debug log level. If your log level is set to :info, then all :info, :notice and above will be passed to backends. If your log level is set to :alert, only :alert and :emergency will be printed.

Add a logging message for the following logging levels in you HelloLoggingWeb.PageController so they will be called when you visit the Phoenix homepage. We’ve also included a call to IO.inspect. Finally, you will need to require Logger.

defmodule HelloLoggingWeb.PageController do
  use HelloLoggingWeb, :controller
  # Add this to make Logger available
  require Logger

  def index(conn, _params) do
    # Add these different logging messages
    IO.inspect("This is IO.inspect")
    Logger.debug("This is a debug log")
    Logger.info("This is an info log")
    Logger.warn("This is a warn log")
    Logger.error("This is an error log")
    render(conn, "index.html")
  end
end

Next visit your homepage again. You should see the following in your console and the different levels may be different colors if your console supports it.

elixir logging

Logger displays all log statements with default settings, but you can adjust the level of log messages Logger sends in your app’s logger configuration. For example, a new Phoenix project is configured to :info log level in production.

# config/prod.exs

# Do not print debug messages in production
config :logger, level: :info

Let’s update our dev config to :warn level to see how it affects our log output.

# config/dev.exs

# Temporarily update dev to :warn level
config :logger, level: :warn

Restart your server and visit the homepage. You should only see the following in your console:

"This is IO.inspect"
[warn] This is a warn log
[error] This is an error log

Now that you’ve seen how we can adjust the log levels in config, remove our updates from config/dev.exs to return to default settings.

Let’s also remove all of the log messages except for the :info level.

defmodule HelloLoggingWeb.PageController do
  use HelloLoggingWeb, :controller
  # Add this to make Logger available
  require Logger

  def index(conn, _params) do
    Logger.info("This is an info log")
    render(conn, "index.html")
  end
end

You should have the default :logger config and a Logger.info message when we visit the homepage.

Logging Metadata

Whenever a message is logged, additional information can be given via metadata. Each log operation, such as Logger.info/2, allows metadata to be given as an argument.

The default config/dev.exs setting removes metadata, so let’s comment that line out to get timestamps and metadata in development.

# config/dev.exs

# Do not include metadata nor timestamps in development logs
# config :logger, :console, format: "[$level] $message\n"

Restart your server and visit the homepage. You should see both timestamps and a request id added to you log output.

20:50:20.907 request_id=FsDUax3mYJj1FHgAAAAm [info] This is an info log
``

To understand our new timestamp and metadata details that have been added to the logs, check out `config/config.exs`.

```elixir
# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

You can see the :format and :metadata settings for a new Phoenix project. You’ll also notice the default settings use the console backend.

We can add our own metadata to the logs. While we haven’t added users, we can simulate adding a user_id to the log metadata. For example, let’s look at the fetch_current_user/2 function added when you run mix phx.gen.auth. See docs on Forbidding access.

# Function from MyProjectWeb.UserAuth when generating auth using mix phx.gen.auth

  def fetch_current_user(conn, _opts) do
    {user_token, conn} = ensure_user_token(conn)
    user = user_token && Accounts.get_user_by_session_token(user_token)
    # Add user_id
    Logger.metadata(user_id: user.id)
    assign(conn, :current_user, user)
  end

You can add the user_id as metadata to help you keep track of users as they visit pages in your app. We haven’t added authentication, so let’s just hard code this is a simple Plug of our own. Let’s add a function plug called fetch_current_user in our HelloLoggingWeb.Endpoint. Then add it before the plug HelloLoggingWeb.Router.

defmodule HelloLoggingWeb.Endpoint do
  ...

  plug :fetch_current_user
  plug HelloLoggingWeb.Router

  def fetch_current_user(conn, _opts) do
    Logger.metadata(user_id: 1)
    conn
  end
end

Now we need to update our config/config.exs file to add the user_id to our metadata.

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id, :user_id]

Restart your server and visit the homepage. Remember, we’ve added a Logger.info message to the homepage. We’ve also commented out the config/dev.exs line that removed metadata from the logging format.

You should see something similar to the following line in your console. All the log messages should contain both the request_id and user_id.

21:08:49.282 request_id=FsEkAb-qx_i7r1UAAABC user_id=1 [info] This is an info log

You can add other metadata to help make your logs easier to search and analyze. Adding a lot of metadata may grow the size of your logs substantially, so you only want to add new metadata that will provide value.

Additional Configuration

Logger supports a wide range of configurations. This configuration is split in three categories:

  • Application configuration, including the option to set your logger backend. The default is the console backend, but you have the option to add an Elixir custom backend.
  • Runtime configuration, including the ability to set the level, log format template, and truncate log messages.
  • Erlang configuration – options that handle integration with Erlang’s logging facilities

Check out the docs for all the configuration options.

Conclusion

Congratulations! You’ve learned how to add logging into your own application. You can set the level and understand how to configure the levels in development and production. You also learned how to add your own metadata to the logs to make it easier for your to analyze logs.

If you liked learning about logging, we have an entire book about managing your app in production. See below for how to get the Quick Reference Guide from the Phoenix Deployment Handbook for free.

Want To Know How To Deploy Phoenix Apps Using A single Command?

This brand-new FREE training reveals the most powerful new way to reduce your deployment time and skyrocket your productivity… and truly see your programming career explode off the charts!