README.md

# 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)
```