OpentelemetryExGram

OpenTelemetry instrumentation for ExGram Telegram bots.

Attaches to ExGram's telemetry events and creates OpenTelemetry spans for update processing, handler execution, middleware, outbound API requests, and polling cycles.

Installation

# mix.exs
def deps do
  [
    {:ex_gram, "~> ..."}, # You should already have ExGram
    {:opentelemetry_ex_gram, "~> 0.1"},
    {:opentelemetry, "~> 1.3"}   # the OTel SDK — add to your application
  ]
end

Setup

Call setup/1 once during application startup, before your bots are started:

def start(_type, _args) do
  OpentelemetryExGram.setup()

  children = [ExGram, {MyBot, [method: :polling, token: token]}]
  Supervisor.start_link(children, strategy: :one_for_one)
end

Options

Option Default Description
:span_prefix"ExGram" Prefix for all span names
:trace_pollingtrue Create spans for polling cycles
:trace_middlewarestrue Create spans for each middleware
:trace_requeststrue Create spans for outbound Telegram API calls

Example with options:

OpentelemetryExGram.setup(
  span_prefix: "MyBot",
  trace_polling: false,
  trace_middlewares: false
)

Span Hierarchy

A typical update produces this span tree:

{prefix}.update      (root, server)
  {prefix}.middleware  (one per middleware, internal)
  {prefix}.handler     (internal — spawned process in :async mode)
    {prefix}.request   (one per outbound Telegram API call, client)

OTel context is propagated automatically across all process boundaries, including spawn/1 for async handlers. No extra configuration needed.

Span Attributes

Span Attributes
updateex_gram.bot, ex_gram.update_id, ex_gram.halted (on stop)
handlerex_gram.bot, ex_gram.handler
middlewareex_gram.bot, ex_gram.middleware, ex_gram.halted (on stop)
pollingex_gram.bot, ex_gram.updates_count (on stop)
requestex_gram.bot, ex_gram.method, ex_gram.request_type

Spans are set to error status (with message) on exception or Telegram API error. Exceptions are also recorded as span events.

Testing

Setting up the span processor

To assert on spans in tests, you need a test span processor that delivers spans to your test process. opentelemetry_test_processor (GitHub) works like Mox for traces.

Add to your deps:

{:opentelemetry_test_processor, "~> 0.1", only: :test}

Configure it in config/test.exs:

config :opentelemetry,
  traces_exporter: :none,
  processors: [{OpenTelemetryTestProcessor, %{}}]

Per-test tracing setup

OpentelemetryExGram.Test.setup/2 attaches the telemetry handler for a specific test bot and registers automatic cleanup when the test exits. It is not a global setup — each test that wants to assert on traces calls it with its own bot_name.

defmodule MyBotTest do
  use ExUnit.Case, async: true
  use ExGram.Test

  alias OpenTelemetryTestProcessor, as: OtelTest
  alias OpenTelemetryTestProcessor.Span

  require Span

  setup {OtelTest, :set_from_context}

  setup context do
    OtelTest.start()
    {bot_name, _} = ExGram.Test.start_bot(context, MyBot)
    OpentelemetryExGram.Test.setup(bot_name)
    {:ok, bot_name: bot_name}
  end

  test "creates a span for each update", %{bot_name: bot_name} do
    ExGram.Test.push_update(bot_name, build_message_update("/start"))

    assert_receive {:trace_span, %Span{name: name} = span}, 1000
    assert name == "#{bot_name}.update"
    assert span.attributes["ex_gram.bot"] == bot_name
  end
end

Custom options per test

Pass any setup/1 option as the second argument:

setup context do
  OtelTest.start()
  {bot_name, _} = ExGram.Test.start_bot(context, MyBot)
  OpentelemetryExGram.Test.setup(bot_name, trace_middlewares: false)
  {:ok, bot_name: bot_name}
end

Manual setup and teardown

For full control, call setup/1 and teardown/1 directly:

{:ok, handler_id} = OpentelemetryExGram.setup(span_prefix: "custom", trace_requests: false)
on_exit(fn -> OpentelemetryExGram.teardown(handler_id) end)