# Telemetry Patterns ## Pattern 1: Dedicated Telemetry Submodule **When to use:** Any module that emits 2+ telemetry events, especially GenServers and pipeline stages. **What it does:** Extracts all `:telemetry.execute/3` calls into a sibling `Telemetry` module. The parent module calls into the telemetry module; the telemetry module owns event names, measurements shape, and metadata contracts. **Structure:** ``` lib/my_app/quote_feed.ex # GenServer — state + logic lib/my_app/quote_feed/telemetry.ex # Telemetry — event emission + docs ``` **Example:** ```elixir defmodule MyApp.QuoteFeed.Telemetry do @moduledoc """ Telemetry events for QuoteFeed. ## Events * `[:my_app, :quote_feed, :connected]` — WebSocket connected. Measurements: `%{system_time: integer()}`. Metadata: `%{url: String.t()}`. * `[:my_app, :quote_feed, :tick, :received]` — Price tick received. Measurements: `%{latency_ms: integer()}`. Metadata: `%{symbol: String.t(), price: Decimal.t()}`. """ @doc "Emit a connection event." def connected(url) do :telemetry.execute( [:my_app, :quote_feed, :connected], %{system_time: System.system_time()}, %{url: url} ) end @doc "Emit a tick received event." def tick_received(symbol, price, latency_ms) do :telemetry.execute( [:my_app, :quote_feed, :tick, :received], %{latency_ms: latency_ms}, %{symbol: symbol, price: price} ) end end ``` **Caller side:** ```elixir defmodule MyApp.QuoteFeed do alias MyApp.QuoteFeed.Telemetry def handle_info({:connected, url}, state) do Telemetry.connected(url) {:noreply, %{state | connected: true}} end end ``` **Why this over inline:** - **Discoverability:** One place lists all events a component emits. Handlers/dashboards have a single source of truth. - **Cohesion:** GenServer focuses on state management. Telemetry module focuses on observability contracts. - **Documentation:** `@moduledoc` on the telemetry module becomes the event catalog. No hunting through callbacks. - **Testability:** You can assert on telemetry calls without coupling to GenServer internals. **When NOT to use:** A module with exactly one telemetry call (e.g., a simple function wrapping a single `:telemetry.execute`). Inline is fine there — the dedicated module adds ceremony without benefit. --- ## Pattern 2: Event Naming Convention **What it does:** Event names follow a hierarchical path: `[app, context, noun, verb_past_tense]`. **Examples:** ```elixir [:gargoyle, :engine, :aggregation, :group, :completed] [:gargoyle, :market_data, :quote_feed, :connected] [:gargoyle, :daily_pnl, :snapshot, :backfilled] ``` **Rules:** - App prefix first (namespace isolation) - Bounded context second (matches code structure) - Noun before verb (what happened to what) - Past tense verbs (events are facts — they already happened) - Never use generic names like `[:my_app, :event]` or `[:my_app, :metric]` **Why:** Consistent naming lets you attach handlers by prefix (`[:gargoyle, :engine | _]`) and build dashboards without memorizing arbitrary event names. --- ## Pattern 3: Measurements vs Metadata Separation **What it does:** Measurements are numeric values you aggregate (sum, avg, p99). Metadata is context for filtering/grouping. ```elixir # Good: clear separation :telemetry.execute( [:my_app, :request, :completed], %{duration_ms: 42, response_bytes: 1024}, # measurements: numbers %{method: :get, path: "/api/v1/users", status: 200} # metadata: dimensions ) # Bad: mixing concerns :telemetry.execute( [:my_app, :request, :completed], %{duration_ms: 42, method: :get}, # method isn't a measurement %{response_bytes: 1024, path: "/api"} # bytes should be a measurement ) ``` **Rule of thumb:** If you'd put it on a Y-axis in a graph → measurement. If you'd use it as a filter/group-by → metadata.