> ## 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 and traces to Bronto

> Instrument Erlang or Elixir BEAM applications with the OpenTelemetry SDK to send logs and traces to Bronto over OTLP via a local Collector.

This page covers instrumenting an Erlang or Elixir application with the OpenTelemetry SDK to send **logs and traces** to Bronto over OTLP/HTTP via a local OTel Collector. Both signals are co-equal: logs are captured via the kernel logger handler automatically, and traces are created using the OTel API macros and functions.

<Tip>
  If you don't have a Collector and want to export directly from your application to Bronto, see [Direct export to Bronto](#direct-export-to-bronto) at the bottom of this page.
</Tip>

## Prerequisites

* Erlang/OTP 24 or later, or Elixir 1.13 or later
* Rebar3 (Erlang) or Mix (Elixir)
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry Erlang/Elixir SDK documentation](https://opentelemetry.io/docs/languages/erlang/)

## Install dependencies

<CodeGroup>
  ```elixir Elixir (mix.exs) theme={"dark"}
  def deps do
    [
      {:opentelemetry_api, "~> 1.4"},
      {:opentelemetry, "~> 1.4"},
      {:opentelemetry_exporter, "~> 1.7"}
    ]
  end
  ```

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

| Package                  | Purpose                                                |
| ------------------------ | ------------------------------------------------------ |
| `opentelemetry_api`      | Core OTel API                                          |
| `opentelemetry`          | SDK — `LoggerProvider`, processors, kernel log handler |
| `opentelemetry_exporter` | OTLP/HTTP exporter                                     |

## Configure the log bridge

The OTel SDK ships an Erlang kernel logger handler (`otel_log_handler`) that captures all log events emitted via `:logger` (Erlang) or `Logger` (Elixir). Configure it in your application config.

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

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

  config :opentelemetry_exporter,
    otlp_protocol: :http_protobuf,
    otlp_logs_endpoint: "http://localhost:4318/v1/logs"
  ```

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

No code changes are required — the kernel logger handler is installed automatically when the `opentelemetry` application starts.

## Set resource attributes

Resource attributes are attached to every log record exported from this process. Two attributes drive how Bronto organises incoming logs:

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

These are set via the `resource` config key in the setup above.

## Complete example

<CodeGroup>
  ```elixir Elixir theme={"dark"}
  # Existing Logger calls work unchanged
  require Logger

  Logger.info("Application started")
  Logger.warning("Low disk space", disk_free_gb: 2.1)
  Logger.error("Database connection failed", reason: :timeout)
  ```

  ```erlang Erlang theme={"dark"}
  %% Existing logger calls work unchanged
  ?LOG_INFO("Application started"),
  ?LOG_WARNING("Low disk space ~p GB free", [2.1]),
  ?LOG_ERROR("Database connection failed: ~p", [timeout]).
  ```
</CodeGroup>

Existing `Logger` / `:logger` calls require no changes. The OTel handler runs alongside any existing handlers (console, file) configured in your kernel logger setup.

## Verify delivery

After running your application, check both signals in Bronto:

* **Logs**: open the [Search](https://app.bronto.io/search) page and filter by the dataset name you set in `service.name` — your log records should appear within a few seconds.
* **Traces**: open the Explore Traces page and filter by the same `service.name` — your spans should appear within a few seconds.

If no logs appear, check:

* The OTel Collector is running and reachable at the configured endpoint.
* The Collector's pipeline includes a `logs` pipeline with an `otlp` receiver and the Bronto exporter — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry).
* The `opentelemetry` application is started before your application (listed in `extra_applications` or `applications` in your `.app.src`/`mix.exs`).

## Traces

<Tip>
  Framework-specific instrumentation libraries are available for Phoenix, Ecto, LiveView, Absinthe, and more — add `opentelemetry_phoenix`, `opentelemetry_ecto`, etc. to your deps and they instrument those frameworks automatically. See [Erlang/Elixir instrumentation libraries](https://opentelemetry.io/docs/languages/erlang/libraries/) for the full list.
</Tip>

### Configure tracing

Tracing is included in the same `opentelemetry` and `opentelemetry_exporter` packages — no additional dependencies are needed. Add the traces endpoint to your config:

<CodeGroup>
  ```elixir Elixir (config/config.exs) theme={"dark"}
  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_traces_endpoint: "http://localhost:4318/v1/traces",
    otlp_logs_endpoint: "http://localhost:4318/v1/logs"
  ```

  ```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_traces_endpoint, "http://localhost:4318/v1/traces"},
    {otlp_logs_endpoint, "http://localhost:4318/v1/logs"}
  ]}
  ```
</CodeGroup>

### Creating spans

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

  Tracer.with_span "process-payment" do
    Tracer.set_attributes([{"payment.amount", 99.99}, {"payment.currency", "USD"}])
    Logger.info("Processing payment")  # trace_id and span_id injected 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 a span will automatically have `trace_id` and `span_id` attached.

## GenAI semantic conventions

If your application calls an LLM, OpenTelemetry defines [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md). There is no first-party GenAI auto-instrumentation package for Erlang/Elixir yet — set the attributes yourself around each LLM call:

<Note>
  This page was verified on **July 17, 2026**. GenAI libraries and semantic conventions are evolving rapidly, so package configuration and emitted attributes can change between releases. Keep your instrumentation current and verify the fields emitted by the version you deploy.
</Note>

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

  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},
      {"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>

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

See [LLM Observability](/ai-features/llm-observability) for the full recommended attribute set and how to search and aggregate GenAI fields in Bronto.

## Direct export to Bronto

If you are not using an OTel Collector, export directly to Bronto by replacing the exporter endpoints and adding your API key:

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

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

| Region | Logs endpoint                            | Traces endpoint                            |
| ------ | ---------------------------------------- | ------------------------------------------ |
| EU     | `https://ingestion.eu.bronto.io/v1/logs` | `https://ingestion.eu.bronto.io/v1/traces` |
| US     | `https://ingestion.us.bronto.io/v1/logs` | `https://ingestion.us.bronto.io/v1/traces` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.
