> ## 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 Python logs and traces to Bronto via OpenTelemetry

> Instrument Python applications with the OpenTelemetry Python SDK and auto-instrumentation to send logs and traces to Bronto over OTLP/HTTP.

This page covers instrumenting a Python 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 bridged from Python's standard `logging` module — no log statement changes needed — and traces are emitted via a tracer provider.

<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

* Python 3.8 or later
* pip
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry Python SDK documentation](https://opentelemetry.io/docs/languages/python/)

## Install dependencies

```bash theme={"dark"}
pip install \
  opentelemetry-api \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-http
```

| Package                                  | Purpose                                              |
| ---------------------------------------- | ---------------------------------------------------- |
| `opentelemetry-api`                      | Core OTel API                                        |
| `opentelemetry-sdk`                      | SDK — `LoggerProvider`, `LoggingHandler`, processors |
| `opentelemetry-exporter-otlp-proto-http` | OTLP/HTTP exporter                                   |

## Configure the log bridge

The OTel Python SDK ships a `LoggingHandler` that bridges Python's standard `logging` module into OTel. Attach it to the root logger (or any specific logger) and all log records flow through the OTel pipeline.

```python theme={"dark"}
import logging
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor

logger_provider = LoggerProvider()

# Attach to the root logger — all loggers in the process inherit this handler
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)
```

<Note>
  Pass a specific logger name to `logging.getLogger("my_app")` if you only want to instrument part of your application.
</Note>

## Configure the OTLP exporter

Wire a `BatchLogRecordProcessor` and `OTLPLogExporter` into the `LoggerProvider`. By default the OTel Collector listens for OTLP/HTTP on port `4318` — no authentication is needed here since the Collector handles the connection to Bronto.

```python theme={"dark"}
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter

exporter = OTLPLogExporter(
    endpoint="http://localhost:4318/v1/logs",
)

logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
```

If your Collector runs on a different host or port, update the endpoint accordingly.

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

Pass a `Resource` when constructing the `LoggerProvider`:

```python theme={"dark"}
from opentelemetry.sdk.resources import Resource

resource = Resource.create({
    "service.name": "my-service",           # → Bronto dataset
    "service.namespace": "my-team",         # → Bronto collection
    "deployment.environment": "production",
})

logger_provider = LoggerProvider(resource=resource)
```

## Complete example

The snippet below puts all the pieces together. Copy it into your application's startup code and call it once before your first log statement.

```python configure_logging.py theme={"dark"}
import logging
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter

def configure_otel_logging():
    resource = Resource.create({
        "service.name": "my-service",
        "service.namespace": "my-team",
        "deployment.environment": "production",
    })

    exporter = OTLPLogExporter(
        endpoint="http://localhost:4318/v1/logs",
    )

    logger_provider = LoggerProvider(resource=resource)
    logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

    handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
    logging.getLogger().addHandler(handler)
    logging.getLogger().setLevel(logging.INFO)


if __name__ == "__main__":
    configure_otel_logging()

    logger = logging.getLogger(__name__)
    logger.info("Application started")
    logger.warning("Low disk space", extra={"disk_free_gb": 2.1})
    logger.error("Database connection failed", exc_info=True)
```

## 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).
* `BatchLogRecordProcessor` exports on a background thread — make sure your process does not exit before the first flush. For short-lived scripts, replace it with `SimpleLogRecordProcessor` to export synchronously.

```python Synchronous export for scripts theme={"dark"}
from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor

# Replace BatchLogRecordProcessor with SimpleLogRecordProcessor
logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
```

## Traces

<Tip>
  For Django, Flask, FastAPI, SQLAlchemy, `requests`, and other popular libraries, the `opentelemetry-instrument` CLI wrapper auto-instruments your app at startup with no code changes — install `opentelemetry-instrumentation` and `opentelemetry-instrumentation-<framework>` (e.g. `opentelemetry-instrumentation-flask`), then run your app via `opentelemetry-instrument python app.py`. See [Python auto-instrumentation](https://opentelemetry.io/docs/zero-code/python/) for setup and the full list of supported libraries.
</Tip>

### Install tracing dependencies

No additional packages are needed — `opentelemetry-sdk` and `opentelemetry-exporter-otlp-proto-http` already include tracing support.

### Configure the tracer provider

Create a `TracerProvider` using the same `Resource` you built for logging, then register it as the global tracer provider.

```python configure_tracing.py theme={"dark"}
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

def configure_otel_tracing(resource):
    exporter = OTLPSpanExporter(
        endpoint="http://localhost:4318/v1/traces",
    )
    provider = TracerProvider(resource=resource)
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)
```

Sharing the `Resource` ensures `service.name` and `service.namespace` are identical on both logs and traces.

### Creating spans

Get a tracer from the global provider and wrap operations in spans:

```python theme={"dark"}
tracer = trace.get_tracer("my-service")

with tracer.start_as_current_span("process-payment") as span:
    span.set_attribute("payment.amount", 99.99)
    span.set_attribute("payment.currency", "USD")
    logger.info("Payment processed")  # trace_id and span_id injected automatically
```

Any log emitted inside an active span automatically receives the `trace_id` and `span_id` — no manual propagation needed.

### Complete example

```python app.py theme={"dark"}
import logging
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

resource = Resource.create({
    "service.name": "my-service",
    "service.namespace": "my-team",
    "deployment.environment": "production",
})

# Logs
log_exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)

# Traces
trace_exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
trace.set_tracer_provider(tracer_provider)

logger = logging.getLogger(__name__)
tracer = trace.get_tracer("my-service")

with tracer.start_as_current_span("handle-request") as span:
    span.set_attribute("http.method", "GET")
    logger.info("Handling request")  # trace_id + span_id attached automatically
```

## GenAI semantic conventions

If your application calls an LLM (OpenAI, Anthropic, Amazon Bedrock, etc.), OpenTelemetry defines a dedicated set of [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) — `gen_ai.*` attributes for model, token usage, and prompt/response content. Python has the richest GenAI auto-instrumentation of any OTel SDK today.

<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>

### Auto-instrumentation

Install the instrumentation package for your provider alongside the SDK packages from [Install dependencies](#install-dependencies):

```bash theme={"dark"}
pip install \
  opentelemetry-instrumentation-openai-v2 \
  opentelemetry-instrumentation-botocore    # Amazon Bedrock, via boto3
```

Then instrument with the zero-code wrapper — no application code changes:

```bash theme={"dark"}
opentelemetry-instrument python app.py
```

This patches the provider SDK client so every model call produces a span such as `chat gpt-4o-mini` carrying `gen_ai.provider.name` (the deprecated `gen_ai.system` on older instrumentation versions), `gen_ai.request.model`, `gen_ai.usage.input_tokens` / `output_tokens`, and `gen_ai.response.finish_reasons`, instead of a plain HTTP client span.

<Note>
  The Anthropic package installed by `pip install opentelemetry-instrumentation-anthropic` is maintained as part of OpenLLMetry, not OTel contrib. See [OpenLLMetry](/integrations/openllmetry) for setup, schema guidance, and content-capture controls.
</Note>

<Tip>
  For LangChain applications, `opentelemetry-instrumentation-langchain` traces chain/runnable steps in addition to the underlying model call — see [LangChain](/integrations/langchain).
</Tip>

### Experimental: capturing prompt and response content

The official Python instrumentations (`openai-v2`, `botocore`) control content capture with two environment variables, both off by default:

```bash theme={"dark"}
# Capture prompt and response content (off by default).
# On opentelemetry-instrumentation-openai-v2 2.x this also accepts the modes
# span_only | event_only | span_and_event (requires the opt-in below).
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only

# Opt in to the latest experimental GenAI conventions. Required for the
# span_only / span_and_event modes; harmless where not read.
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
```

<Warning>
  Where content lands depends on the mode. With the legacy `true` setting, content is emitted as **log records** (OTel log events such as `gen_ai.user.message` / `gen_ai.choice`) — it flows through the logs pipeline, correlated to the span by `trace_id`, and is not queryable on the span itself. With `span_only` (or `span_and_event`), content is written **directly to span attributes** (`gen_ai.input.messages` / `gen_ai.output.messages`), which Bronto surfaces as searchable trace fields — prefer this mode where your instrumentation supports it.

  Where it doesn't, emit the content as a structured **log record** correlated by `trace_id`, using the log bridge configured above. See [LLM Observability](/ai-features/llm-observability) for the recommended attribute names, a worked logging example, and Bronto search queries.
</Warning>

### Manual spans

Where no auto-instrumentation package exists for your provider, set the `gen_ai.*` attributes yourself on a span, following the same conventions:

```python theme={"dark"}
tracer = trace.get_tracer("my-service")

with tracer.start_as_current_span("chat gpt-4o-mini") as span:
    span.set_attribute("gen_ai.provider.name", "openai")
    span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
    span.set_attribute("gen_ai.usage.input_tokens", 33)
    span.set_attribute("gen_ai.usage.output_tokens", 74)
    # An array attribute — Bronto displays it flattened as finish_reasons.0
    span.set_attribute("gen_ai.response.finish_reasons", ["stop"])
```

See [LLM Observability](/ai-features/llm-observability) for the full recommended attribute set, including `gen_ai.input.messages` / `gen_ai.output.messages`, 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:

```python theme={"dark"}
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

log_exporter = OTLPLogExporter(
    endpoint="https://ingestion.eu.bronto.io/v1/logs",  # or ingestion.us.bronto.io
    headers={"x-bronto-api-key": "<YOUR_API_KEY>"},
)

trace_exporter = OTLPSpanExporter(
    endpoint="https://ingestion.eu.bronto.io/v1/traces",  # or ingestion.us.bronto.io
    headers={"x-bronto-api-key": "<YOUR_API_KEY>"},
)
```

| 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.
