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
]
endSetup
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)
endOptions
| Option | Default | Description |
|---|---|---|
:span_prefix | "ExGram" | Prefix for all span names |
:trace_polling | true | Create spans for polling cycles |
:trace_middlewares | true | Create spans for each middleware |
:trace_requests | true | 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 |
|---|---|
update | ex_gram.bot, ex_gram.update_id, ex_gram.halted (on stop) |
handler | ex_gram.bot, ex_gram.handler |
middleware | ex_gram.bot, ex_gram.middleware, ex_gram.halted (on stop) |
polling | ex_gram.bot, ex_gram.updates_count (on stop) |
request | ex_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
endCustom 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}
endManual 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)