ALLM.Telemetry (allm v0.3.0)

Copy Markdown View Source

Internal — Layer B helper that wraps :telemetry.span/3 with ALLM-specific metadata defaults.

Phase 9.1 wires this around every public Layer-C entry point (generate, stream_generate, step, stream_step, chat, stream) so callers can attach :telemetry.attach_many/4 handlers to [:allm, :generate | :stream | :step | :chat | :tool | :image, :start | :stop | :exception] and observe every execution with :duration plus :request_id, :engine, :model metadata. See spec §29.

Phase 14.3 added the :image span name for the ALLM.generate_image/3 · edit_image/4 · image_variations/3 Layer-C façade. The :image span's :stop measurements include :image_count (count of images in the response, 0 on error per design Decision #8); metadata extends with :operation (:generate | :edit | :variation), :n (requested count), :usage, :response, and :error. See spec §35.9.

Why request_id is metadata-only

:request_id lives on telemetry span metadata and on Response.request_id post-collection — it does NOT extend the ALLM.Event closed tagged-tuple union. A consumer who folds events by hand (without ALLM.StreamCollector) reads :request_id from the surrounding span context, not from individual event payloads. See Phase 9 design Non-obvious Decision #1 for the full rationale.

Span nesting

Per-tool spans ([:allm, :tool, ...]) execute inside their parent step span ([:allm, :step, ...]); the parent step span itself nests inside its parent chat span when invoked from chat/3 / stream/3. Inheritance: request_id is generated at the outermost call and threaded into inner calls via opts[:request_id].

Exception handling

span/3 delegates exception trapping to :telemetry.span/3 (telemetry 1.2+) which automatically catches raises in the closure, emits [:allm, name, :exception] with %{kind, reason, stacktrace} metadata, and re-raises the exception to the caller — preserving the existing bubble-up semantics of every wrapped function.

Summary

Types

Common metadata attached to every Layer-C span (spec §29).

Span suffix per spec §29 (chat) and §35.9 (image); the prefix [:allm] is fixed.

Functions

Return the fixed event-name prefix for every ALLM telemetry event.

Emit a single non-span event under the [:allm | suffix_path] event name. Used by ALLM.Retry for [:allm, :adapter, :retry]. No metadata merging or measurement injection — the caller supplies both maps in full.

Generate a fresh 22-character URL-safe Base64 request id.

Wrap a closure in a :telemetry.span/3 call under the [:allm, name] prefix.

Types

common_metadata()

@type common_metadata() :: %{
  optional(:request_id) => String.t(),
  optional(:engine) => term(),
  optional(:model) => term(),
  optional(atom()) => term()
}

Common metadata attached to every Layer-C span (spec §29).

span_name()

@type span_name() :: :generate | :stream | :step | :chat | :tool | :image

Span suffix per spec §29 (chat) and §35.9 (image); the prefix [:allm] is fixed.

Functions

event_prefix()

@spec event_prefix() :: [:allm]

Return the fixed event-name prefix for every ALLM telemetry event.

Examples

iex> ALLM.Telemetry.event_prefix()
[:allm]

execute(suffix_path, measurements, metadata)

@spec execute([atom(), ...], map(), map()) :: :ok

Emit a single non-span event under the [:allm | suffix_path] event name. Used by ALLM.Retry for [:allm, :adapter, :retry]. No metadata merging or measurement injection — the caller supplies both maps in full.

Examples

iex> :telemetry.attach(
...>   "doctest-execute",
...>   [:allm, :doctest, :ping],
...>   fn _n, _m, _md, %{owner: pid} -> send(pid, :got_event) end,
...>   %{owner: self()}
...> )
iex> ALLM.Telemetry.execute([:doctest, :ping], %{count: 1}, %{tag: :doc})
:ok
iex> :telemetry.detach("doctest-execute")
iex> receive do
...>   :got_event -> :ok
...> after
...>   100 -> :no_event
...> end
:ok

request_id()

@spec request_id() :: String.t()

Generate a fresh 22-character URL-safe Base64 request id.

Sourced from 16 cryptographic random bytes. Used to correlate the start, stop, and any exception events of a single Layer-C call. The outermost public function generates the id; inner functions inherit via opts[:request_id] (see Phase 9 Non-obvious Decision #7).

Examples

iex> id = ALLM.Telemetry.request_id()
iex> byte_size(id)
22
iex> id =~ ~r/^[A-Za-z0-9_-]{22}$/
true

span(name, start_metadata, fun)

@spec span(
  span_name(),
  common_metadata(),
  (-> {result, map()} | {result, map(), map()})
) :: result
when result: var

Wrap a closure in a :telemetry.span/3 call under the [:allm, name] prefix.

The closure must return either:

  • {result, stop_metadata_extras} — the 2-tuple form (default, used by :generate | :stream | :step | :chat | :tool spans). stop_metadata_extras is shallow-merged on top of the start metadata at :stop emission time so per-span extras (:response, :step_result, :chat_result, :result) appear alongside the common keys.
  • {result, extra_measurements, stop_metadata_extras} — the 3-tuple form, for spans that inject custom :stop measurements beyond :duration and :monotonic_time. Phase 14.3 added this for :image spans which carry :image_count as a measurement per design Decision #8 (telemetry-stdlib idiom: numeric metrics → measurements; structured context → metadata).

Caller-supplied start_metadata is forwarded to the :start event unchanged and used as the base for the :stop event's metadata.

Raises ArgumentError for an unrecognised name (typo guard against :chats / :steps); valid names are :generate | :stream | :step | :chat | :tool | :image.

Carve-out: :stream :stop :response is nil

Per ALLM.StreamRunner.run/3, the :stream span's :stop metadata carries :response => nil — materialising the wrapped enumerable to populate the %Response{} would defeat consumer-driven laziness. Consumers needing the canonical response should either fold the returned stream themselves (ALLM.StreamCollector.to_response/1) or call ALLM.generate/3, whose :generate :stop DOES carry the reduced %Response{}. See review Finding #2.

Examples

iex> :telemetry.attach(
...>   "doctest-handler",
...>   [:allm, :generate, :stop],
...>   fn _name, _measurements, %{result: r}, %{owner: pid} ->
...>     send(pid, {:doctest_event, r})
...>   end,
...>   %{owner: self()}
...> )
iex> ALLM.Telemetry.span(:generate, %{request_id: "x"}, fn ->
...>   {:done, %{result: :ok}}
...> end)
:done
iex> :telemetry.detach("doctest-handler")
iex> receive do
...>   {:doctest_event, payload} -> payload
...> after
...>   100 -> :no_event
...> end
:ok