Installation
Add clarion to your mix.exs dependencies:
def deps do
[
{:clarion, "~> 0.1.0"}
]
endBasic 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
endIn 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=:timeoutAnywhere 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