Telemetry

Copy Markdown

Sycophant emits :telemetry events at key points in the request lifecycle, following the standard span pattern. An optional OpenTelemetry bridge translates these events into OTel spans with GenAI semantic conventions.

Events

Request Lifecycle

  • [:sycophant, :request, :start] -- request begins

    • Measurements: %{system_time: integer}
    • Metadata: %{model, provider, wire_protocol, has_tools?, has_stream?, temperature, top_p, top_k, max_tokens}
  • [:sycophant, :request, :stop] -- request succeeds

    • Measurements: %{duration: integer} (native time units)
    • Metadata: start metadata merged with %{duration, usage, response_model, response_id, finish_reason}
    • Usage includes token counts, cache token counts, and cost fields
  • [:sycophant, :request, :error] -- request fails

    • Measurements: %{duration: integer} (native time units)
    • Metadata: start metadata merged with %{error, error_class}

Streaming

  • [:sycophant, :stream, :chunk] -- individual stream chunk received
    • Measurements: %{}
    • Metadata: %{chunk_type: atom}

Embeddings

  • [:sycophant, :embedding, :start] -- embedding request begins
  • [:sycophant, :embedding, :stop] -- embedding request succeeds
  • [:sycophant, :embedding, :error] -- embedding request fails

Attaching Handlers

:telemetry.attach_many(
  "my-sycophant-handler",
  Sycophant.Telemetry.events(),
  &handle_event/4,
  nil
)

defp handle_event([:sycophant, :request, :stop], measurements, metadata, _config) do
  Logger.info(
    "LLM request to #{metadata.model} took #{measurements.duration} " <>
    "and used #{metadata.usage[:total_tokens]} tokens"
  )
end

Telemetry Placement

Telemetry events only fire for requests that pass parameter validation and credential resolution. Invalid requests fail fast without emitting events. This means your telemetry handlers only see real API calls, not configuration errors.

Usage Metadata

The usage field in stop metadata contains:

KeyDescription
:input_tokensTokens in the prompt
:output_tokensTokens in the completion
:total_tokensSum of input and output (computed)
:cache_creation_input_tokensTokens written to provider cache
:cache_read_input_tokensTokens read from provider cache
:reasoning_tokensInternal reasoning tokens (thinking models)
:input_costCost of input tokens (from LLMDB pricing)
:output_costCost of output tokens
:cache_read_costCost of cache read tokens
:cache_write_costCost of cache creation tokens
:reasoning_costCost of reasoning tokens
:total_costSum of all cost components
:pricingFull pricing metadata as a plain map (see Pricing guide)

OpenTelemetry Integration

Sycophant includes an optional OpenTelemetry bridge that creates OTel spans from telemetry events, following the GenAI semantic conventions.

Setup

Add the optional dependency to your mix.exs:

{:opentelemetry_telemetry, "~> 1.1"}

Then call setup in your application startup:

# In your Application.start/2
Sycophant.OpenTelemetry.setup()

Span Attributes

Start attributes follow GenAI conventions:

AttributeSource
gen_ai.operation.name"chat" or "embeddings"
gen_ai.provider.nameProvider atom as string
gen_ai.request.modelRequested model identifier
gen_ai.request.temperatureTemperature parameter
gen_ai.request.top_pTop-p parameter
gen_ai.request.top_kTop-k parameter
gen_ai.request.max_tokensMax tokens parameter

Stop attributes:

AttributeSource
gen_ai.usage.input_tokensInput token count
gen_ai.usage.output_tokensOutput token count
gen_ai.usage.cache_creation.input_tokensCache creation tokens
gen_ai.usage.cache_read.input_tokensCache read tokens
gen_ai.response.modelActual model used
gen_ai.response.idProvider response ID
gen_ai.response.finish_reasonsFinish reason(s)

Custom Attributes

Pass an attribute_mapper function to enrich spans with application-specific attributes:

Sycophant.OpenTelemetry.setup(
  attribute_mapper: fn metadata ->
    [
      {"app.tenant_id", metadata[:tenant_id]},
      {"app.feature", metadata[:feature]}
    ]
  end
)

Span Propagation

The OTel bridge creates child spans of whatever trace context exists in the calling process. If your Phoenix controller or LiveView already has an active span, Sycophant spans will appear as children automatically.

Teardown

Sycophant.OpenTelemetry.teardown()