# OpentelemetryExGram
OpenTelemetry instrumentation for [ExGram](https://github.com/rockneurotiko/ex_gram) 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
```elixir
# 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:
```elixir
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_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:
```elixir
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`](https://hex.pm/packages/opentelemetry_test_processor) ([GitHub](https://github.com/rockneurotiko/opentelemetry_test_processor)) works like Mox for traces.
Add to your deps:
```elixir
{:opentelemetry_test_processor, "~> 0.1", only: :test}
```
Configure it in `config/test.exs`:
```elixir
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`.
```elixir
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:
```elixir
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:
```elixir
{:ok, handler_id} = OpentelemetryExGram.setup(span_prefix: "custom", trace_requests: false)
on_exit(fn -> OpentelemetryExGram.teardown(handler_id) end)
```