Packages

Elixir client library for cryptocurrency exchanges — generated from CCXT specs via compile-time macros.

Current section

Files

Jump to
ccxt_client lib ccxt telemetry.ex
Raw

lib/ccxt/telemetry.ex

defmodule CCXT.Telemetry do
@moduledoc """
Centralized telemetry contract for CCXT.
This module is the single source of truth for all telemetry events emitted by
the CCXT library. Both `CCXT.HTTP.Client` and `CCXT.CircuitBreaker` delegate
their event names to this module.
## Contract Version
The contract version (`contract_version/0`) is bumped on breaking changes to
event names, measurements, or metadata shapes. This allows consumers to assert
compatibility at startup.
## Request Events
Emitted by `CCXT.HTTP.Client` during HTTP request lifecycle.
### `[:ccxt, :request, :start]`
Emitted before the HTTP request is sent.
- **Measurements:** `%{system_time: integer()}`
- **Metadata:** `%{exchange: atom(), method: atom(), path: String.t()}`
### `[:ccxt, :request, :stop]`
Emitted after a successful HTTP response (any status code).
- **Measurements:** `%{duration: integer()}` (native time units)
- **Metadata:** `%{exchange: atom(), method: atom(), path: String.t(), status: integer()}`
- **Optional metadata:** `rate_limit: %CCXT.HTTP.RateLimitInfo{}` when the
exchange returns rate limit headers
### `[:ccxt, :request, :exception]`
Emitted when the request raises or returns a transport/request error.
- **Measurements:** `%{duration: integer()}` (native time units)
- **Metadata:** `%{exchange: atom(), method: atom(), path: String.t(), kind: atom(), reason: term()}`
## Circuit Breaker Events
Emitted by `CCXT.CircuitBreaker` on state transitions.
### `[:ccxt, :circuit_breaker, :open]`
Emitted when a circuit breaker opens after exceeding the failure threshold.
- **Measurements:** `%{system_time: integer()}`
- **Metadata:** `%{exchange: atom()}`
### `[:ccxt, :circuit_breaker, :closed]`
Emitted when a circuit breaker resets (closes) after the reset timeout.
- **Measurements:** `%{system_time: integer()}`
- **Metadata:** `%{exchange: atom()}`
### `[:ccxt, :circuit_breaker, :rejected]`
Emitted when a request is rejected because the circuit is open.
- **Measurements:** `%{system_time: integer()}`
- **Metadata:** `%{exchange: atom()}`
"""
@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]
@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
]
@all_events @request_events ++ @circuit_breaker_events
# ============================================================================
# Contract Version
# ============================================================================
@doc """
Returns the telemetry contract version.
Bumped on breaking changes to event names, measurements, or metadata shapes.
Consumers can assert compatibility at startup:
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
# ============================================================================
# Event Lists
# ============================================================================
@doc "Returns all 6 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
# ============================================================================
# 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 (for later detach)
- `handler_fn` - Function of arity 4: `(event, measurements, metadata, config)`
- `config` - Optional handler config passed as the 4th argument to `handler_fn` (default: `nil`)
## Example
CCXT.Telemetry.attach("my-logger", fn event, measurements, metadata, _config ->
Logger.info("CCXT event: \#{inspect(event)}")
end)
"""
@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.
Wraps `:telemetry.detach/1`.
"""
@spec detach(String.t()) :: :ok | {:error, :not_found}
def detach(handler_id) when is_binary(handler_id) do
:telemetry.detach(handler_id)
end
end