> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bronto.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Erlang and Elixir logs, metrics, and traces to Bronto

> Instrument Erlang or Elixir BEAM applications with the OpenTelemetry SDK to send logs, metrics, and traces to Bronto over OTLP/HTTP, via an OTel Collector or directly.

On the BEAM, OpenTelemetry is configured entirely in application config — no initialisation code. One config block sends **logs and traces** to Bronto: the SDK installs a kernel logger handler that captures every `Logger` / `:logger` call, and framework instrumentation libraries produce spans for Phoenix, Ecto, and friends.

The application exports to an [OpenTelemetry Collector](/agent-setup/open-telemetry), which forwards to Bronto. If you don't run a Collector, [export directly to Bronto](#direct-export-to-bronto) instead — only the endpoint and an API key header change.

<Note>
  Metrics on the BEAM live in the separate `opentelemetry_experimental` package and the instrument API is still unstable. Logs and traces, covered below, are stable. See [metrics](#metrics) at the end of this page.
</Note>

## Prerequisites

* Erlang/OTP 24 or later, or Elixir 1.13 or later
* An OTel Collector reachable from your application, with `logs`, `metrics`, and `traces` pipelines forwarding to Bronto — see [Connect OpenTelemetry Collector to Bronto](/agent-setup/open-telemetry)

## Install dependencies

<CodeGroup>
  ```elixir Elixir (mix.exs) theme={"dark"}
  def deps do
    [
      {:opentelemetry_api, "~> 1.4"},
      {:opentelemetry, "~> 1.4"},
      {:opentelemetry_exporter, "~> 1.7"},
      # framework instrumentation — add what applies
      {:opentelemetry_phoenix, "~> 1.2"},
      {:opentelemetry_ecto, "~> 1.2"}
    ]
  end
  ```

  ```erlang Erlang (rebar.config) theme={"dark"}
  {deps, [
    {opentelemetry_api, "~> 1.4"},
    {opentelemetry, "~> 1.4"},
    {opentelemetry_exporter, "~> 1.7"}
  ]}.
  ```
</CodeGroup>

See [Erlang/Elixir instrumentation libraries](https://opentelemetry.io/docs/languages/erlang/libraries/) for the full list, including LiveView and Absinthe.

## Configure the SDK

<CodeGroup>
  ```elixir Elixir (config/config.exs) theme={"dark"}
  import Config

  config :opentelemetry,
    resource: [
      service: [name: "my-service", namespace: "my-team"],
      "deployment.environment": "production"
    ],
    traces_exporter: :otlp,
    logs_exporter: :otlp

  config :opentelemetry_exporter,
    otlp_protocol: :http_protobuf,
    otlp_endpoint: "http://localhost:4318"
  ```

  ```erlang Erlang (sys.config) theme={"dark"}
  [
    {opentelemetry, [
      {resource, #{
        <<"service.name">>           => <<"my-service">>,
        <<"service.namespace">>      => <<"my-team">>,
        <<"deployment.environment">> => <<"production">>
      }},
      {traces_exporter, otlp},
      {logs_exporter, otlp}
    ]},
    {opentelemetry_exporter, [
      {otlp_protocol, http_protobuf},
      {otlp_endpoint, "http://localhost:4318"}
    ]}
  ].
  ```
</CodeGroup>

That's the whole setup. `otlp_endpoint` is a base URL — the exporter appends `/v1/logs` and `/v1/traces` itself. The kernel logger handler is installed when the `opentelemetry` application starts, so make sure it is listed in `extra_applications` (Elixir) or `applications` (Erlang) and starts before your own.

Two resource attributes determine how Bronto organises your data:

| OTel attribute      | Bronto concept | Description                                  |
| ------------------- | -------------- | -------------------------------------------- |
| `service.name`      | Dataset        | Groups telemetry from one service            |
| `service.namespace` | Collection     | Groups related services or a team's services |

<Note>
  `http://localhost:4318` is the standard OTLP/HTTP address for a Collector on the same host. Use the address reachable from your application if the Collector runs in another container, pod, or host. No authentication is needed between the application and the Collector — the Collector holds the Bronto API key.
</Note>

## Instrument your application

Existing `Logger` / `:logger` calls need no changes — the OTel handler runs alongside your console and file handlers. For spans around your own business logic:

<CodeGroup>
  ```elixir Elixir theme={"dark"}
  require Logger
  require OpenTelemetry.Tracer, as: Tracer

  Tracer.with_span "process-payment" do
    Tracer.set_attributes([{"payment.amount", 99.99}])
    Logger.info("Processing payment")  # trace_id and span_id attached automatically
  end
  ```

  ```erlang Erlang theme={"dark"}
  ?with_span(<<"process-payment">>, #{}, fun(_SpanCtx) ->
    ?set_attributes([{<<"payment.amount">>, 99.99}]),
    ?LOG_INFO("Processing payment")
  end)
  ```
</CodeGroup>

Any log emitted inside an active span automatically carries that span's `trace_id` and `span_id`, so you can jump from a log line to its trace in Bronto.

## Verify

Run your application, then check each signal in Bronto, filtering by the `service.name` you set:

* **Logs** — the [Search](https://app.bronto.io/search) page, in the dataset named after your service
* **Traces** — the Explore Traces page

If nothing arrives:

* Confirm the Collector is running and reachable at `otlp_endpoint`, and that its pipelines include an `otlp` receiver and the Bronto exporters.
* Confirm the `opentelemetry` application starts before your own application.
* Port `4318` is OTLP/HTTP and `4317` is OTLP/gRPC. If you point at `4317`, set `otlp_protocol: :grpc`.

For signal-specific reference material, see [Send Traces to Bronto](/tracing/send-traces).

## Metrics

BEAM metrics require the `opentelemetry_experimental` package, which provides the meter and instrument API and a periodic reader. Reuse the same `resource` and OTLP exporter config as above, and check the [OpenTelemetry Erlang/Elixir documentation](https://opentelemetry.io/docs/languages/erlang/) for the instrument API in the release you deploy.

An established alternative on the BEAM is to export existing `telemetry` metrics through `TelemetryMetricsPrometheus` and scrape them with the Collector's `prometheus` receiver, which forwards them to Bronto through the same pipeline. See [Send Metrics to Bronto](/metrics/send-metrics).

## Direct export to Bronto

Without a Collector, the application exports straight to Bronto over OTLP/HTTP. Point the endpoint at your Bronto region and add your API key:

<CodeGroup>
  ```elixir Elixir (config/config.exs) theme={"dark"}
  config :opentelemetry_exporter,
    otlp_protocol: :http_protobuf,
    otlp_endpoint: "https://ingestion.eu.bronto.io",   # or ingestion.us.bronto.io
    otlp_headers: [{"x-bronto-api-key", "<YOUR_API_KEY>"}]
  ```

  ```erlang Erlang (sys.config) theme={"dark"}
  {opentelemetry_exporter, [
    {otlp_protocol, http_protobuf},
    {otlp_endpoint, "https://ingestion.eu.bronto.io"},
    {otlp_headers, [{<<"x-bronto-api-key">>, <<"<YOUR_API_KEY>">>}]}
  ]}
  ```
</CodeGroup>

| Region | Base endpoint                    |
| ------ | -------------------------------- |
| EU     | `https://ingestion.eu.bronto.io` |
| US     | `https://ingestion.us.bronto.io` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions.

<Tip>
  Prefer the Collector for multi-service environments — it batches, filters, and enriches telemetry, and keeps the API key out of every application. Direct export suits single services and simple architectures.
</Tip>

## GenAI semantic conventions

If your application calls an LLM, OpenTelemetry's [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) define `gen_ai.*` span attributes for model, token usage, and prompt and response content.

### Manual spans

There is no first-party GenAI instrumentation for Erlang or Elixir, so set the attributes yourself around each model call:

<CodeGroup>
  ```elixir Elixir theme={"dark"}
  Tracer.with_span "chat gpt-4o-mini" do
    Tracer.set_attributes([
      {"gen_ai.provider.name", "openai"},
      {"gen_ai.request.model", "gpt-4o-mini"},
      {"gen_ai.usage.input_tokens", 33},
      {"gen_ai.usage.output_tokens", 74},
      # Bronto flattens array attributes — searchable as finish_reasons.0
      {"gen_ai.response.finish_reasons", ["stop"]}
    ])
  end
  ```

  ```erlang Erlang theme={"dark"}
  ?with_span(<<"chat gpt-4o-mini">>, #{}, fun(_SpanCtx) ->
    ?set_attributes([
      {<<"gen_ai.provider.name">>, <<"openai">>},
      {<<"gen_ai.request.model">>, <<"gpt-4o-mini">>},
      {<<"gen_ai.usage.input_tokens">>, 33},
      {<<"gen_ai.usage.output_tokens">>, 74},
      {<<"gen_ai.response.finish_reasons">>, [<<"stop">>]}
    ])
  end)
  ```
</CodeGroup>

Message content (`gen_ai.input.messages` / `gen_ai.output.messages`) is opt-in by convention and off by default in the languages that have auto-instrumentation. Since you are setting attributes by hand, apply the same discipline — gate prompt and response content behind your own config flag rather than always sending it.

See the [GenAI span conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) for the full attribute list.

For the recommended attribute set and Bronto search queries, see [LLM Observability](/ai-features/llm-observability).
