defmodule CCXT.Telemetry do @moduledoc """ Centralized telemetry contract for CCXT. Single source of truth for all telemetry events emitted by the library. Both `CCXT.HTTP` and `CCXT.CircuitBreaker` delegate event names here. ## Contract Version Bumped on breaking changes to event names, measurements, or metadata shapes. Consumers can assert compatibility at startup. ## Request Events Emitted by `CCXT.HTTP` during HTTP request lifecycle. ### `[:ccxt, :request, :start]` - **Measurements:** `%{system_time: integer()}` - **Metadata:** `%{exchange: String.t(), method: atom(), path: String.t()}` ### `[:ccxt, :request, :stop]` - **Measurements:** `%{duration: integer()}` (native time units) - **Metadata:** `%{exchange: String.t(), method: atom(), path: String.t(), status: integer()}` ### `[:ccxt, :request, :exception]` - **Measurements:** `%{duration: integer()}` (native time units) - **Metadata:** `%{exchange: String.t(), method: atom(), path: String.t(), kind: atom(), reason: term()}` ## Circuit Breaker Events Emitted by `CCXT.CircuitBreaker` on state transitions. ### `[:ccxt, :circuit_breaker, :open]` - **Measurements:** `%{system_time: integer()}` - **Metadata:** `%{exchange: String.t()}` ### `[:ccxt, :circuit_breaker, :closed]` - **Measurements:** `%{system_time: integer()}` - **Metadata:** `%{exchange: String.t()}` ### `[:ccxt, :circuit_breaker, :rejected]` - **Measurements:** `%{system_time: integer()}` - **Metadata:** `%{exchange: String.t()}` ## Rate Limiter Events Emitted by `CCXT.HTTP` when rate limiting is triggered. ### `[:ccxt, :rate_limiter, :throttled]` - **Measurements:** `%{delay_ms: integer(), cost: number()}` - **Metadata:** `%{exchange: String.t()}` """ @contract_version 1 @request_start_event [:ccxt, :request, :start] @request_stop_event [:ccxt, :request, :stop] @request_exception_event [:ccxt, :request, :exception] @circuit_breaker_open_event [:ccxt, :circuit_breaker, :open] @circuit_breaker_closed_event [:ccxt, :circuit_breaker, :closed] @circuit_breaker_rejected_event [:ccxt, :circuit_breaker, :rejected] @rate_limiter_throttled_event [:ccxt, :rate_limiter, :throttled] @request_events [@request_start_event, @request_stop_event, @request_exception_event] @circuit_breaker_events [ @circuit_breaker_open_event, @circuit_breaker_closed_event, @circuit_breaker_rejected_event ] @rate_limiter_events [@rate_limiter_throttled_event] @all_events @request_events ++ @circuit_breaker_events ++ @rate_limiter_events # ============================================================================ # Contract Version # ============================================================================ @doc """ Returns the telemetry contract version. Bumped on breaking changes to event names, measurements, or metadata shapes. if CCXT.Telemetry.contract_version() != 1 do raise "Incompatible CCXT telemetry contract" end """ @spec contract_version() :: pos_integer() def contract_version, do: @contract_version # ============================================================================ # Event Name Functions # ============================================================================ @doc "Event name for request start: `[:ccxt, :request, :start]`." @spec request_start() :: [atom()] def request_start, do: @request_start_event @doc "Event name for request stop: `[:ccxt, :request, :stop]`." @spec request_stop() :: [atom()] def request_stop, do: @request_stop_event @doc "Event name for request exception: `[:ccxt, :request, :exception]`." @spec request_exception() :: [atom()] def request_exception, do: @request_exception_event @doc "Event name for circuit breaker open: `[:ccxt, :circuit_breaker, :open]`." @spec circuit_breaker_open() :: [atom()] def circuit_breaker_open, do: @circuit_breaker_open_event @doc "Event name for circuit breaker closed: `[:ccxt, :circuit_breaker, :closed]`." @spec circuit_breaker_closed() :: [atom()] def circuit_breaker_closed, do: @circuit_breaker_closed_event @doc "Event name for circuit breaker rejected: `[:ccxt, :circuit_breaker, :rejected]`." @spec circuit_breaker_rejected() :: [atom()] def circuit_breaker_rejected, do: @circuit_breaker_rejected_event @doc "Event name for rate limiter throttled: `[:ccxt, :rate_limiter, :throttled]`." @spec rate_limiter_throttled() :: [atom()] def rate_limiter_throttled, do: @rate_limiter_throttled_event # ============================================================================ # Event Lists # ============================================================================ @doc "Returns all telemetry event names." @spec events() :: [[atom()]] def events, do: @all_events @doc "Returns the 3 HTTP request event names." @spec request_events() :: [[atom()]] def request_events, do: @request_events @doc "Returns the 3 circuit breaker event names." @spec circuit_breaker_events() :: [[atom()]] def circuit_breaker_events, do: @circuit_breaker_events @doc "Returns the rate limiter event names." @spec rate_limiter_events() :: [[atom()]] def rate_limiter_events, do: @rate_limiter_events # ============================================================================ # Convenience API # ============================================================================ @doc """ Attaches a handler to all CCXT telemetry events. Wraps `:telemetry.attach_many/4` with `events/0` as the event list. ## Parameters - `handler_id` - Unique string identifying this handler - `handler_fn` - Function of arity 4: `(event, measurements, metadata, config)` - `config` - Optional handler config (default: `nil`) """ @spec attach(String.t(), (list(), map(), map(), term() -> any()), term()) :: :ok | {:error, :already_exists} def attach(handler_id, handler_fn, config \\ nil) when is_binary(handler_id) and is_function(handler_fn, 4) do :telemetry.attach_many(handler_id, events(), handler_fn, config) end @doc """ Detaches a previously attached handler by ID. """ @spec detach(String.t()) :: :ok | {:error, :not_found} def detach(handler_id) when is_binary(handler_id) do :telemetry.detach(handler_id) end end