Installation

Add clarion to your mix.exs dependencies:

def deps do
  [
    {:clarion, "~> 0.1.0"}
  ]
end

Basic usage

Clarion provides macros, so you require it the same way you'd require Logger:

defmodule MyHttpClient do
  require Clarion

  def request(url) do
    case do_request(url) do
      {:ok, response} ->
        Clarion.info(%{event: :http_request_succeeded, url: url, status: response.status})
        {:ok, response}

      {:error, reason} ->
        Clarion.error(%{event: :http_request_failed, url: url, reason: reason})
        {:error, reason}
    end
  end
end

In a dev console, that renders (via default_report_cb/1) as:

http_request_succeeded url=https://example.com status=200
http_request_failed url=https://example.com reason=:timeout

Anywhere a JSON handler (logger_json, logger_formatter_json, etc.) is attached, the same call arrives with url, status, and reason as real fields — not text to re-parse.

Clarion.report/3 directly

info/2, warning/2, and error/2 are thin wrappers over report/3 for the three most common levels. Any Logger.level/0 works via report/3 directly:

Clarion.report(:debug, %{event: :cache_lookup, key: key, hit?: hit?})
Clarion.report(:critical, %{event: :pool_exhausted, pool: pool_name})

Supplying your own report_cb

The default rendering is a reasonable key=value fallback, not the only option. Pass your own renderer via the :report_cb option — it follows Erlang's :logger report_cb() 1-arity contract: given the report, return {format_string, args}:

Clarion.error(
  %{event: :http_request_failed, url: url, reason: reason},
  report_cb: fn %{url: url, reason: reason} ->
    {~c"request to ~s failed: ~p", [url, reason]}
  end
)

No matter what your report_cb does — including if it's buggy, slow, or never called by anything — the raw map is what's actually logged. Your report_cb is stored in the log event's metadata for a consumer to call if it wants text; Clarion itself never calls it. See how_logging_actually_works.md for exactly why that separation matters.

Passing through additional metadata

Any option other than :report_cb is forwarded as :logger metadata, exactly like the second argument to Logger.log/3:

Clarion.info(%{event: :cache_hit, key: key}, request_id: conn.assigns.request_id)

Verifying structure survives, in your own tests

If you want to prove (not just assume) that a report your library emits stays structured, attach a real :logger handler in your test and assert on what it actually receives — the same technique Clarion's own test suite uses on itself:

defmodule MyStructuredConsumer do
  def log(log_event, %{config: %{owner: owner}}), do: send(owner, {:log_event, log_event})
end

setup do
  :logger.add_handler(:test_consumer, MyStructuredConsumer, %{
    config: %{owner: self()},
    filter_default: :log
  })

  on_exit(fn -> :logger.remove_handler(:test_consumer) end)
end

test "emits a structured http_request_failed report" do
  MyHttpClient.request("https://example.invalid")

  assert_receive {:log_event, %{msg: {:report, %{event: :http_request_failed}}}}
end