axelson

axelson

Scenic Core Team

Can you use ex_unit to assert that a log statement was emitted with specific metadata?

I often use ExUnit.CaptureLog to capture information about a log message. But it doesn’t give you any information about the logged metadata. I’ve been adding structured logging recently and I’d like to assert that my code is correctly emitting the correct structured logs, but ExUnit.CaptureLog doesn’t keep or return the metadata. How have others solved this? Maybe we could create a PR to ex_unit that would add a function that returns the metadata along with the captured message?

Marked As Solved

RudManusachi

RudManusachi

Hi, Jason :wave: !

In one project I format logs in json and include metadata there in the message, so it’s pretty straight forward… (Jason.decode the captured log and assert on internals).

However, I guess that’s not your case, so I think we could add :logger handler and try to assert on the metadata within log_event.
Since handlers run in the same process as the one that emits the log we could send the log_event to self() and receive in test roughly something like this:

defmodule LoggerTestHelper do
  def log(log_event, config) do
    send(config.test_pid, {:logged_event, log_event})
  end
end

defmodule Test do
  use ExUnit.Case, async: true
  require Logger

  setup do
    :logger.add_handler(:test_handler, LoggerTestHelper, %{test_pid: self()})
    on_exit(fn -> :logger.remove_handler(:test_handler) end)
  end

  test "metadata" do
    Logger.info("hello", foo: :bar)

    assert_receive {:logged_event, %{meta: meta} = _log_event}
    assert meta.foo == :bar
  end
end

UPD: I just realized, that event might be logged by a process other than the test itself, and sending log_event to self() is quite limiting… Hence, I updated the example passing test_pid to logger handler config and having it to send log_event to test_pid explicitly.

Also Liked

axelson

axelson

Scenic Core Team

Hi Rudolf! :wave:

Oooh, I didn’t expect adding a handler within the test to end up that straightforward and clean! I’ve marked your answer as the solution because it does indeed solve the problem, thank you!

I’ve also extended your example helper slightly to improve the ergonomics, mainly so that it’s easy to get all the logged messages that match a given string, and to return a formatted version of the string (although I’m sure that there’s other cases that would be needed besides {:string, iolist} if this were to hit production:

defmodule LoggerTestHelper do
  def log(log_event, config) do
    message =
      case log_event.msg do
        {:string, iolist} -> to_string(iolist)
      end

    send(config.test_pid, {:logged_event, message, log_event})
  end

  def logs_containing_string(search_string) do
    {:messages, all_messages} = :erlang.process_info(self(), :messages)

    for {:logged_event, message, log_event} <- all_messages,
        String.contains?(message, search_string) do
      {message, log_event}
    end
  end
end

And here’s my example test:

test "logs with metadata", %{conn: conn} do
  {_result, _log} =
    ExUnit.CaptureLog.with_log(fn ->
      conn = get(conn, ~p"/posts")
      assert html_response(conn, 200) =~ "Listing Posts"
    end)

  assert [{message, log}] =
           LoggerTestHelper.logs_containing_string("Sent 200 for GET /posts")

  assert message =~ "2 queries executed in"
  assert message =~ "2 logs captured"

  assert log.meta.method == "GET"
  assert log.meta.request_path == "/posts"
  assert log.meta.duration
  assert log.meta.total_queries == 2

  for query <- log.meta.ecto_queries do
    assert query =~ "FROM \"posts\""
  end
end

If you’re curious about the content of the test I’ve been playing with the ideas of Wide Logs (or Canonical Logs) that are captured quite well in this Stripe blog post:

Where Next?

Popular in Questions Top

belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement