code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when adding logging, metrics, or instrumentation — Logger metadata, :telemetry handlers, Telemetry.Metrics tags, Ecto/Phoenix events, LiveDashboard, OpenTelemetry. Not for deployment config (use deployment-gotchas).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill telemetry-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/telemetry-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when adding logging, metrics, or instrumentation — Logger metadata, :telemetry handlers, Telemetry.Metrics tags, Ecto/Phoenix events, LiveDashboard, OpenTelemetry. Not for deployment config (use deployment-gotchas).
name: telemetry-essentials description: "Use when adding logging, metrics, or instrumentation — Logger metadata, :telemetry handlers, Telemetry.Metrics tags, Ecto/Phoenix events, LiveDashboard, OpenTelemetry. Not for deployment config (use deployment-gotchas)."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14 — October 2026
1. **Put values you'll filter on in Logger metadata; a human-readable message may interpolate** — `Logger.info("Order #{order.id} created", order_id: order.id)`; `require Logger` first, and list the metadata keys in the formatter config or they're dropped 2. **Configure log output with `config :logger, :default_formatter` (or `:default_handler` for a JSON formatter)** — `config :logger, :console` is ignored when `:default_formatter` is set, which Phoenix 1.8's `config.exs` does 3. **Don't overwrite `request_id`** — `Plug.RequestId` (in every generated endpoint) already sets it as Logger metadata; it is not in `conn.assigns` 4. **Attach handlers once at boot, with `&__MODULE__.handle_event/4` and a catch-all clause** — a handler that raises is detached permanently and silently; a local capture (`&handle_event/4`) makes telemetry warn about a performance penalty 5. **Use the events Ecto, Phoenix and Oban already emit** — don't wrap queries manually; of Ecto's query measurements only `total_time` is always present, so read the others with `Map.get/2` 6. **Metric tags must have low cardinality** — route, status, queue, `source` yes; `user_id`, `request_id`, email never. Per-user ids belong in log metadata and traces 7. **If the project has its own telemetry wrapper or event catalog, use it** instead of calling `:telemetry.execute/3` directly 8. **Logger metadata is per process** — `Task`, `GenServer` and LiveView processes don't inherit it; pass it on explicitly 9. **LiveDashboard stays inside the generated `dev_routes` block, or behind admin auth** — never in a scope with only `:browser` 10. **Never log at `:debug` in production** — it includes query parameters (see the `deployment-gotchas` skill)
---
Metadata is what log tools index and filter on. The message is for humans and may interpolate.
<!-- epg:bad -->
# Nothing to filter on, and inspect(user) dumps the whole struct
Logger.info("User #{inspect(user)} created order")require Logger
Logger.info("Order #{order.id} created", order_id: order.id, user_id: order.user_id)
Logger.error("Payment failed: #{inspect(reason)}", order_id: order.id, reason: inspect(reason))Metadata keys not listed in the formatter config are silently dropped:
# config/config.exs — Phoenix 1.8 generates this block; extend the list config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", metadata: [:request_id, :user_id, :order_id]
[logger_json](https://hex.pm/packages/logger_json) 7 plugs into Elixir's default handler:
# mix.exs
{:logger_json, "~> 7.0"}
# config/runtime.exs
if config_env() == :prod do
config :logger, :default_handler,
formatter: LoggerJSON.Formatters.Basic.new(metadata: [:request_id, :user_id, :order_id])
end`Basic.new/1` is only available in `runtime.exs` (deps are loaded by then). In `config.exs`/`prod.exs` use the tuple form `{LoggerJSON.Formatters.Basic, metadata: [...]}`. Formatters for Datadog, Google Cloud and Elastic exist alongside `Basic`.
<!-- epg:bad -->
# Ignored: :console is copied to :default_formatter only when :default_formatter # is unset, and Phoenix 1.8's config.exs sets it config :logger, :console, format: "$message\n", metadata: [:request_id]
`request_id` is already set by `Plug.RequestId`. Add the user once the scope is known, in a router plug and in a LiveView `on_mount` hook. A LiveView runs in its own process, so plug metadata doesn't reach it.
# router.ex — after :fetch_current_scope_for_user in the :browser pipeline
plug :put_user_log_metadata
defp put_user_log_metadata(conn, _opts) do
with %{user: %{id: user_id}} <- conn.assigns[:current_scope] do
Logger.metadata(user_id: user_id)
end
conn
enddefmodule MyAppWeb.LogMetadata do
def on_mount(:default, _params, _session, socket) do
with %{user: %{id: user_id}} <- socket.assigns[:current_scope] do
Logger.metadata(user_id: user_id)
end
{:cont, socket}
end
end
# router.ex — after the hook that assigns current_scope
live_session :require_authenticated_user,
on_mount: [{MyAppWeb.UserAuth, :require_authenticated}, MyAppWeb.LogMetadata] do
# live routes
end`Logger.metadata/1` is a plain function, so it needs no `require Logger` (an unused `require` is a warning).
metadata = Logger.metadata() Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> Logger.metadata(metadata) deliver_webhook(event) end)
The same applies to Oban jobs: put the ids you need in the job args and set metadata in `perform/1`. OpenTelemetry carries trace context across processes, but only in code instrumented to propagate it.
---
# name (list of atoms), measurements (map), metadata (map)
:telemetry.execute([:my_app, :orders, :created], %{count: 1, total_cents: 4999}, %{source: :web})# lib/my_app/application.ex
@impl true
def start(_type, _args) do
MyApp.TelemetryHandlers.attach()
children = [
MyAppWeb.Telemetry,
MyApp.Repo,
MyAppWeb.Endpoint
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
enddefmodule MyApp.TelemetryHandlers do
require Logger
@events [
[:my_app, :orders, :created],
[:my_app, :payments, :failed]
]
def attach do
:telemetry.attach_many("my-app-handlers", @events, &__MODULE__.handle_event/4,· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…