Skip to main content

README.md

# Clarion

[![Hex.pm](https://img.shields.io/hexpm/v/clarion.svg)](https://hex.pm/packages/clarion)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/clarion)
[![CI](https://github.com/singlefleck/clarion/actions/workflows/ci.yml/badge.svg)](https://github.com/singlefleck/clarion/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Correct structured logging for library authors: readable in a dev console,
fully queryable in production, and immune to the one failure mode that
usually destroys it — a caller's `report_cb` can never cause the original
structured data to be lost, because Clarion never calls it itself.

## The problem

```elixir
# naive: readable now, unrecoverable later
Logger.info("retrying request to #{url}, attempt #{attempt}/#{max}")
```

Fine in a terminal. Once this event reaches a JSON handler (Datadog, Loki,
anything doing programmatic log analysis), `url`, `attempt`, and `max` are
gone — flattened into one string before `:logger` ever saw them as data.
No formatter, no handler, no clever config downstream can get them back.

```elixir
# Clarion: same information, still structured everywhere it goes
require Clarion

Clarion.error(%{event: :http_retry, url: url, attempt: attempt, max: max})
```

Every consumer of this event gets what it actually wants: a console
handler renders `http_retry url=... attempt=2 max=3` via
`Clarion.default_report_cb/1`; a JSON handler gets real `url`/`attempt`/
`max` fields to filter, alert, and query on.

## Why this needs a library at all

Erlang's `:logger` already supports structured reports and a `report_cb`
rendering callback — this isn't new machinery. What's missing is a call
site API that makes the *correct* pattern the only reachable one:
`Clarion.info/2`, `.warning/2`, and `.error/2` require a map or keyword
list, not a string, so there's no code path where a report gets
interpolated into text before it ever reaches `:logger`.

Read [`docs/how_logging_actually_works.md`](docs/how_logging_actually_works.md)
for the verified, experiment-backed explanation of exactly where structure
gets destroyed in the current `Logger`/`:logger` pipeline, and
[`docs/comparison.md`](docs/comparison.md) for how Clarion differs from
JSON formatters, text formatters, and log-shipping backends that already
exist on Hex — short version: those all operate on an event *after* it
exists; Clarion operates at the call site that creates it.

## Installation

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

## Quick start

```elixir
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
```

See [`docs/getting_started.md`](docs/getting_started.md) for custom
`report_cb`s, passing extra metadata, and testing that your own logging
stays structured.

## Documentation

Full docs, including all extras above, are on
[HexDocs](https://hexdocs.pm/clarion).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT — see [LICENSE](LICENSE).