Current section
Files
Jump to
Current section
Files
lib/ccxt/telemetry.ex
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