defmodule HuggingfaceClient.Inference.Telemetry do
@moduledoc """
Telemetry integration for `HuggingfaceClient`.
## Events emitted
All events share the prefix `[:huggingface_client, :request]`.
| Event | When |
|-------|------|
| `[:huggingface_client, :request, :start]` | Before the HTTP call |
| `[:huggingface_client, :request, :stop]` | After a successful HTTP call |
| `[:huggingface_client, :request, :exception]` | If the call raises |
### Measurements
- `:duration` — wall-clock time in native units (`:stop` and `:exception` only)
- `:monotonic_time` — start time (`:start` only)
### Metadata
| Key | Description |
|-----|-------------|
| `:provider` | Provider string, e.g. `"groq"` |
| `:task` | Task string, e.g. `"conversational"` |
| `:model` | Model ID or `nil` |
| `:status` | HTTP status code (`:stop` only) |
| `:error` | Exception struct (`:exception` only) |
## Attaching a handler
HuggingfaceClient.Inference.Telemetry.attach_default_logger()
This logs a single line per request at `:info` level using `Logger`.
## Custom handler example
:telemetry.attach(
"my-hf-metrics",
[:huggingface_client, :request, :stop],
fn _event, %{duration: duration}, %{provider: provider, task: task}, _ ->
MyMetrics.histogram("hf_inference.request_duration",
duration,
tags: [provider: provider, task: task]
)
end,
nil
)
"""
require Logger
@start_event [:huggingface_client, :request, :start]
@stop_event [:huggingface_client, :request, :stop]
@exception_event [:huggingface_client, :request, :exception]
@all_events [@start_event, @stop_event, @exception_event]
@doc """
Attaches a default structured logger handler.
Logs `:info` on success, `:warning` on exceptions.
Safe to call multiple times — detaches the old handler first.
"""
@spec attach_default_logger(atom()) :: :ok
def attach_default_logger(handler_id \\ :huggingface_client_default_logger) do
:telemetry.detach(handler_id)
:telemetry.attach_many(
handler_id,
@all_events,
&__MODULE__.handle_event/4,
%{log_level: :info}
)
:ok
end
@doc "Detaches the default logger."
@spec detach_default_logger(atom()) :: :ok | {:error, :not_found}
def detach_default_logger(handler_id \\ :huggingface_client_default_logger) do
:telemetry.detach(handler_id)
end
@doc "Returns all telemetry events emitted by this library."
@spec events() :: [[atom()]]
def events, do: @all_events
@doc false
@spec handle_event([atom()], map(), map(), map()) :: :ok
def handle_event(@stop_event, %{duration: duration}, meta, %{log_level: level}) do
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.log(
level,
"[HuggingfaceClient] #{meta.provider}/#{meta.task} #{meta.model} #{ms}ms status=#{meta[:status] || "ok"}"
)
end
def handle_event(@exception_event, %{duration: duration}, %{error: error} = meta, _) do
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.warning(
"[HuggingfaceClient] #{meta.provider}/#{meta.task} #{meta.model} #{ms}ms error=#{inspect(error)}"
)
end
def handle_event(@start_event, _, _, _), do: :ok
end