# Monitor a Python app with OpenTelemetry

> Install opentelemetry-distro and start FastAPI or Django with opentelemetry-instrument. Requests, queries, errors and logs reach Lampwick, no code changes.

OpenTelemetry's zero-code instrumentation for Python sends requests, database queries, errors and `logging` records from a FastAPI or Django app to Lampwick. You start the app with `opentelemetry-instrument` and set environment variables; the code stays as it is. The general reference is [OpenTelemetry](/docs/opentelemetry/).

Tested with Python 3.9.6, `opentelemetry-distro` 0.62b1 (`opentelemetry-sdk` and `opentelemetry-exporter-otlp-proto-http` 1.41.1), FastAPI 0.128 on uvicorn 0.39, and Django 4.2 with SQLite, on 10 November 2026.

## Requirements

- Python 3.9 or later.
- The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it.

## Let your agent do it

Paste this into your coding agent, in the app's repository:

```text
Add OpenTelemetry to this Python app so it reports to Lampwick: follow https://lampwick.dev/docs/python.md exactly (opentelemetry-distro, opentelemetry-bootstrap, opentelemetry-instrument, the environment variables). Read the ingest token from the environment, never commit it.
```

## Install the packages

In the app's virtualenv, after your app's own dependencies:

```bash
pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http
opentelemetry-bootstrap -a install
```

`opentelemetry-bootstrap` looks at what's installed and adds the matching instrumentations: for our FastAPI app `opentelemetry-instrumentation-fastapi`, `-asgi`, `-sqlite3` and `-logging`, for Django `opentelemetry-instrumentation-django`. Run it again after you add a framework or database driver. To pin them, `opentelemetry-bootstrap -a requirements` prints the list for `requirements.txt`.

## Set the environment variables

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN
export OTEL_SERVICE_NAME=my-app
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=otlp
```

`YOUR_PROJECT_TOKEN` is the project's ingest token. `OTEL_LOGS_EXPORTER=otlp` is all logs need: the `logging` instrumentation sends every record that passes your loggers' level.

## FastAPI

Start uvicorn through `opentelemetry-instrument`:

```bash
opentelemetry-instrument uvicorn main:app --port 8000
```

The app we tested:

```python
import logging
import sqlite3

from fastapi import FastAPI

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("shop")
app = FastAPI()

db = sqlite3.connect("shop.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, total INTEGER)")
db.execute("INSERT OR IGNORE INTO orders VALUES (1, 10), (2, 20)")
db.commit()

@app.get("/orders/{order_id}")
def show_order(order_id: int):
    cursor = db.cursor()
    cursor.execute("SELECT id, total FROM orders WHERE id = ?", (order_id,))
    row = cursor.fetchone()
    logger.info("order %s looked up", order_id)
    return {"id": row[0], "total": row[1]} if row else None

@app.get("/boom")
def boom():
    raise RuntimeError("Checkout failed for order 42")

@app.get("/handled")
def handled():
    try:
        1 / 0
    except ZeroDivisionError:
        logger.exception("could not compute the discount")
    return {"ok": True}
```

After opening `/orders/1`, `/orders/2`, `/boom` and `/handled`, the project shows:

- **Requests**: `GET /orders/{order_id}` (2), `GET /boom` (status 500) and `GET /handled`.
- **Queries**: `SELECT id, total FROM orders WHERE id = ?`, linked to its request.
- **Issues**: `RuntimeError: Checkout failed for order 42`, unhandled, at `main.py:27`; and `ZeroDivisionError: division by zero`, handled, from `logger.exception`.
- **Logs**: 2 × `order … looked up` (info) and `could not compute the discount` (error), each linked to its request.

## Django

Django's instrumentation needs `DJANGO_SETTINGS_MODULE` in the environment, and the development server's autoreloader has to be off:

```bash
export DJANGO_SETTINGS_MODULE=mysite.settings
opentelemetry-instrument python manage.py runserver --noreload
```

With views that run a query (through `django.db.connection` or the ORM), log with `logging` and raise an exception, the project shows:

- **Requests**: `GET /orders/<int:order_id>` and `GET /boom` (status 500), named after the URL pattern.
- **Queries**: every SQL statement, linked to its request, plus the queries Django runs at startup (without a request).
- **Issues**: `RuntimeError: Checkout failed for order 42`, unhandled, once per request. Django also logs it as `Internal Server Error: /boom` on the `django.request` logger; that line shows under **Logs**, and Lampwick counts the error once.

We tested `runserver`. With gunicorn or uWSGI, the worker processes fork after the SDK starts; the OpenTelemetry docs describe a `post_fork` hook for that, which we haven't tested.

## What's missing

- **`connection.execute()` on `sqlite3` isn't traced**, only `cursor.execute()`. Django and SQLAlchemy use cursors, so this only affects raw `sqlite3` code.
- **Info logs need a level.** Python's root logger drops records below `WARNING` unless you configure it (`logging.basicConfig(level=logging.INFO)`, or `LOGGING` in Django).
- **`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`** is the old switch. It still works, but logs a deprecation warning into your logs; leave it out.

## Troubleshooting

| What you see | Cause and fix |
|---|---|
| Django requests don't arrive, other spans do | `DJANGO_SETTINGS_MODULE` isn't set in the environment `opentelemetry-instrument` runs in, or the autoreloader is on: add `--noreload`. |
| `Skipping installation of LoggingHandler … to avoid duplicate logs` in your logs | `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true` is set. Remove it. |
| `NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+` | macOS's system Python is built with LibreSSL. The export still works; use a Python from python.org or Homebrew to get rid of it. |
| Queries are missing | The driver's instrumentation isn't installed. Run `opentelemetry-bootstrap -a install` again after installing the driver. |
