Skip to main content

README.md

# FosferonTelemetry

Shared failure-telemetry conventions for Elixir apps: UTF-8-safe metadata
shaping, shorthand counter generation, and a generic `:telemetry` handler for
the canonical failure-event schema.

The problem this solves: failure events are easy to emit but hard to ship
safely. A naive truncation splits a UTF-8 codepoint and breaks every
JSON-serializing consumer (Prometheus, OpenTelemetry, log shippers). A
`Postgrex.Error` or `Ecto.QueryError` happily embeds raw SQL — including bind
values — straight into your metrics pipeline. This library centralises the
defensive shaping so every app in an organisation wires the same safe
metadata before values reach a sink.

## Installation

```elixir
def deps do
  [
    {:fosferon_telemetry, "~> 0.1"}
  ]
end
```

## The canonical failure schema

Every failure event carries the same metadata shape:

| key            | type     | notes                                        |
|----------------|----------|----------------------------------------------|
| `stage`        | `atom`   | The only field safe to use as a metric **tag**. |
| `error_class`  | `atom`   | Coarse category: `:timeout`, `:db`, `:auth`, … |
| `error_message`| `string` | Bounded, sanitised — no SQL, no query AST.   |

The event-name convention is `[:<app>, <domain>, :failure]` (3-segment) — for
example `[:my_app, :checkout, :failure]`. Apps with deeper event hierarchies
can use the explicit-tuple mode of `FailureMetrics`.

## `FosferonTelemetry.Util` — safe metadata shaping

The defensive core. Use it before any value lands in telemetry metadata.

```elixir
# UTF-8-safe truncation — never splits a codepoint.
FosferonTelemetry.Util.truncate("x" <> "\u201C" <> String.duplicate("y", 1000), 64)
# => a valid UTF-8 prefix of <= 64 bytes, followed by " [truncated]"

# Exception sanitisation — never leaks SQL or query AST.
err = %Postgrex.Error{
  postgres: %{message: "relation \"users\" does not exist"},
  query: "SELECT * FROM users WHERE token = 'secret'"
}
FosferonTelemetry.Util.safe_exception_message(err)
# => "relation \"users\" does not exist"   — query body is dropped

FosferonTelemetry.Util.safe_exception_message(%Ecto.QueryError{message: "...SQL..."})
# => "Ecto.QueryError"   — the AST-bearing message is replaced wholesale
```

`safe_exception_message/1` handles `%Postgrex.Error{}` (extracts only
`postgres.message`), `%Ecto.QueryError{}` (returns the class name), other
exceptions (via `Exception.message/1`), binaries, atoms, and arbitrary terms.

## `FosferonTelemetry.FailureMetrics` — counter generation

Builds `Telemetry.Metrics.counter/2` definitions from compact input.

```elixir
# Shorthand: one app, many domains — each becomes a 3-segment failure event.
FosferonTelemetry.FailureMetrics.counters(:my_app, [:session, :checkout, :support])
# => [
#   counter("my_app.session.failure.count", tags: [:stage]),
#   counter("my_app.checkout.failure.count", tags: [:stage]),
#   counter("my_app.support.failure.count", tags: [:stage])
# ]

# Explicit tuples: for 4–5 segment event names that don't fit the shorthand.
FosferonTelemetry.FailureMetrics.counters([
  {[:billing, :invoices, :refund, :error], "billing.invoices.refund.error.count"},
  {[:billing, :invoices, :line_items, :create, :error], "billing.invoices.line_items.create.error.count"}
])
```

Hand the result to your Telemetry reporter (e.g. `telemetry_metrics_prometheus`
or `telemetry_metrics_statsd`) in your supervisor.

### Tag discipline

Counters use **only `:stage`** as a tag. Never tag on IDs, slugs, or free-text
fields — unbounded cardinality kills Prometheus. This is enforced by design in
both counter modes.

## `FosferonTelemetry.FailureHandler` — generic logger

A single `:telemetry` handler that logs every canonical failure event at
`:warning` level. Attach once per app; it survives its own crashes.

```elixir
# In Application.start/2 or a Telemetry supervisor:
FosferonTelemetry.FailureHandler.attach(:my_app, [:session, :checkout, :support])
```

Properties worth knowing:

- **Idempotent** — `:already_exists` from `:telemetry.attach_many/4` is treated
  as `:ok`, so supervision re-init and application restarts are safe.
- **Self-recovering** — the handler rescues its own exceptions so a bad event
  payload can't cause `:telemetry` to detach it. A detached handler would
  silently drop every subsequent failure event for the app.

The handler ID follows `"<app>-fosferon-failure-handler"`.

## License

MIT — see [LICENSE](LICENSE).