Merge branch 'master' into clarify-sse-event-terminator

This commit is contained in:
Yurii Motov authored and GitHub committed 2026-10-07 09:50:59 +02:00
commit f74008774b
82 files changed
+8000 -601

No files matched your search

+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
@@ -88,7 +88,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+3 -3
View File
@@ -28,14 +28,14 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Bump pre-commit hooks
run: uv run prek auto-update --freeze --cooldown-days 7
run: uv run prek update --freeze --cooldown-days 7
- name: Get PR Submit token
id: pr-submit
uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1
@@ -62,7 +62,7 @@ jobs:
--base "$BASE_BRANCH" \
--head "$branch" \
--title "⬆ Bump pre-commit hooks" \
--body "Bump pre-commit hook versions via \`prek auto-update --freeze --cooldown-days 7\`." \
--body "Bump pre-commit hook versions via \`prek update --freeze --cooldown-days 7\`." \
--label internal \
--label dependencies \
--label pre-commit
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
- name: Extract release details
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: false
@@ -61,7 +61,7 @@ jobs:
env:
PROJECT_NAME: fastapitiangolo
BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }}
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} # zizmor: ignore[secrets-outside-env]
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # zizmor: ignore[secrets-outside-env]
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
cache-dependency-glob: |
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
- name: Prepare release
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: "false"
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
cache-dependency-glob: |
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+4 -4
View File
@@ -115,7 +115,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
@@ -177,7 +177,7 @@ jobs:
with:
python-version: "3.13"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
@@ -245,7 +245,7 @@ jobs:
python-version-file: "base/.python-version"
- name: Setup uv
if: steps.changed-tests.outputs.found == 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
@@ -284,7 +284,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
enable-cache: true
+2 -2
View File
@@ -53,7 +53,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
cache-dependency-glob: |
@@ -95,7 +95,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "latest-known"
cache-dependency-glob: |
+1 -1
View File
@@ -22,4 +22,4 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
+1 -1
View File
@@ -15,7 +15,7 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/crate-ci/typos
rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # frozen: v1.49.0
rev: 00f422f3b19c57bc6338715ebfe3316d38768461 # frozen: v1.50.3
hooks:
- id: typos
args: [--force-exclude]
-2
View File
@@ -53,7 +53,6 @@ The key features are:
<a href="https://blockbee.io?ref=fastapi" target="_blank" title="BlockBee Cryptocurrency Payment Gateway"><img src="https://fastapi.tiangolo.com/img/sponsors/blockbee.png"></a>
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
<a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" target="_blank" title="Deploy & scale any full-stack web app on Render. Focus on building apps, not infra."><img src="https://fastapi.tiangolo.com/img/sponsors/render.svg"></a>
<a href="https://www.coderabbit.ai/?utm_source=fastapi&utm_medium=badge&utm_campaign=fastapi" target="_blank" title="Cut Code Review Time & Bugs in Half with CodeRabbit"><img src="https://fastapi.tiangolo.com/img/sponsors/coderabbit.png"></a>
<a href="https://subtotal.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=open-source" target="_blank" title="The Gold Standard in Retail Account Linking"><img src="https://fastapi.tiangolo.com/img/sponsors/subtotal.svg"></a>
<a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" target="_blank" title="Deploy enterprise applications at startup speed"><img src="https://fastapi.tiangolo.com/img/sponsors/railway.png"></a>
@@ -64,7 +63,6 @@ The key features are:
<a href="https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display" target="_blank" title="Pay as you go for market data"><img src="https://fastapi.tiangolo.com/img/sponsors/databento.svg"></a>
<a href="https://www.svix.com/" target="_blank" title="Svix - Webhooks as a service"><img src="https://fastapi.tiangolo.com/img/sponsors/svix.svg"></a>
<a href="https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi" target="_blank" title="Fine-Grained Authorization for FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/permit.png"></a>
<a href="https://dribia.com/en/" target="_blank" title="Dribia - Data Science within your reach"><img src="https://fastapi.tiangolo.com/img/sponsors/dribia.png"></a>
<a href="https://www.bairesdev.com/" target="_blank" title="BairesDev | Nearshore Software Development & Staff Augmentation Company"><img src="https://fastapi.tiangolo.com/img/sponsors/bairesdev.svg"></a>
<a href="https://tutorcruncher.com/?utm_source=fastapi" target="_blank" title="TutorCruncher"><img src="https://fastapi.tiangolo.com/img/sponsors/tutorcruncher.png"></a>
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Wenn Ihre API läuft, möchten Sie vielleicht wissen, wie viel Traffic sie erhält, welche Requests langsam sind und wann Fehler auftreten.
**Telemetrie** umfasst Daten über das Verhalten Ihrer Anwendung, die Ihnen helfen, diese Fragen zu beantworten. Zu den gängigen Typen gehören:
- **Metriken**: Messwerte, die Sie über einen Zeitraum zusammenfassen können, etwa Responsezeiten und die Anzahl der verarbeiteten Requests.
- **Traces**: Aufzeichnungen einzelner Requests und der Operationen, die zu ihrer Verarbeitung ausgeführt werden. Jede zeitlich erfasste Operation wird als **Span** bezeichnet.
- **Logs**: Aufzeichnungen von Events mit Zeitstempel, etwa das Starten einer Anwendung oder das Fehlschlagen einer Operation.
[**OpenTelemetry**](https://opentelemetry.io/) ist eine Sammlung von Standards und Werkzeugen zum Erfassen von Telemetriedaten und zum Senden dieser Daten an einen Monitoring-Dienst, wo Sie sie in Dashboards untersuchen können.
**FastAPI bietet standardmäßig OpenTelemetry-Unterstützung** für HTTP-Request-Traces, Metriken und Logs. WebSocket-Verbindungen liefern ebenfalls Traces und Logs. Um diese Daten anzuzeigen, konfigurieren Sie einen Monitoring-Dienst, der sie empfängt.
## FastAPI installieren { #install-fastapi }
Installieren Sie FastAPI mit den `standard`-Extras, die die Pakete zum Senden von Telemetriedaten enthalten:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Die Anwendung erstellen { #create-the-app }
Erstellen Sie eine Datei `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Beachten Sie, dass alles standardmäßig funktioniert. Sie müssen keinen eigenen Code schreiben, damit die Telemetrie funktioniert.
## FastAPI Cloud { #fastapi-cloud }
Wenn Sie mit `fastapi[standard]` auf [FastAPI Cloud](https://fastapicloud.com) deployen, funktionieren Metriken automatisch. Sie müssen nichts weiter konfigurieren.
Mit einem Pro-Tarif können Sie die Anzahl der Requests, Fehlerraten und Responsezeiten im [Metriken-Dashboard](https://fastapicloud.com/docs/monitoring-and-performance/metrics/) anzeigen.
<img src="/img/tutorial/opentelemetry/image01.png" alt="FastAPI Cloud Pro-Metriken-Dashboard mit Beispieldaten">
## Andere Monitoring-Dienste { #other-monitoring-services }
Um Telemetriedaten an einen anderen Monitoring-Dienst zu senden, konfigurieren Sie einen Endpunkt, der **OTLP** akzeptiert, das OpenTelemetry-Protokoll zum Senden von Telemetriedaten. Verwenden Sie den HTTP/protobuf-Basisendpunkt des Dienstes.
Setzen Sie diese Umgebungsvariablen und ersetzen Sie die Beispiel-URL durch Ihren Endpunkt:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` identifiziert Ihre Anwendung im Monitoring-Dienst. Der Endpunkt ist die Basis-URL für den Empfang von Daten. Unter dieser URL werden Traces an `/v1/traces`, Metriken an `/v1/metrics` und Logs an `/v1/logs` gesendet.
Wenn Ihr Dienst Authentifizierung erfordert, setzen Sie `OTEL_EXPORTER_OTLP_HEADERS` auf die von ihm vorgegebenen Header, zum Beispiel `api-key=YOUR_API_KEY`.
## Die Anwendung ausführen { #run-the-app }
Starten Sie die Anwendung im selben Terminal:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
Senden Sie in einem anderen Terminal einen Request:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Öffnen Sie Ihren Monitoring-Dienst und suchen Sie nach `my-api`. Nach dem nächsten Export sehen Sie einen Trace mit einem `GET /items/{item_id}`-Span sowie Metriken zur Anzahl der Requests, zur Responsedauer und zu aktiven Requests.
## Telemetrie anpassen { #customize-telemetry }
### Provider und Exporter konfigurieren { #configure-providers-and-exporters }
Ein **Provider** stellt die Objekte bereit, die Traces, Metriken oder Logs aufzeichnen. Seine Konfiguration steuert, wie diese Daten verarbeitet und exportiert werden.
Telemetriebibliotheken können die globalen Provider von OpenTelemetry konfigurieren. Konfigurieren Sie die Bibliothek, bevor die Anwendung startet, und FastAPI verwendet diese Provider automatisch.
Wenn ein OTLP-Endpunkt in der Umgebung gesetzt ist, fügt FastAPI jedem aktivierten Provider einen Exporter für dieses Ziel hinzu. Vorhandene Exporter senden weiterhin Daten an ihre Ziele.
Konfigurieren Sie jedes Ziel einmal. Wenn eine andere Bibliothek bereits das in der Umgebung angegebene Ziel verwaltet, deaktivieren Sie deren Export über die Umgebung oder schalten Sie FastAPIs automatische Einrichtung aus:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Sie können auch einen Provider direkt im Dictionary `telemetry` übergeben. Dieser Provider verwendet beispielsweise den Konsolenexporter von OpenTelemetry, um Request-Spans in Ihrem Terminal auszugeben:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
Der **Exporter** sendet die Spans an ihr Ziel. `BatchSpanProcessor` gruppiert Spans und sendet sie im Hintergrund. Ersetzen Sie den Konsolenexporter durch einen Exporter Ihrer Monitoring-Bibliothek, um deren Ziel zu verwenden. Weitere Konfigurationsoptionen finden Sie im [Leitfaden zur Python-Instrumentierung von OpenTelemetry](https://opentelemetry.io/docs/languages/python/instrumentation/).
Verwenden Sie `meter_provider` oder `logger_provider` im selben Dictionary, um einen Provider für Metriken oder Logs bereitzustellen. Die Anwendung oder Bibliothek, die einen Provider erstellt, verwaltet dessen Shutdown. FastAPI verwaltet die Exportkomponenten, die es hinzufügt.
/// warning | Achtung
OpenTelemetry verwendet standardmäßig globale Provider. Eine unabhängige Telemetriekonfiguration für [gemountete Unteranwendungen](sub-applications.md) ist nicht garantiert.
///
### Request-Operationen nachverfolgen { #trace-request-operations }
Standardmäßig enthalten Request-Traces Spans für das Auflösen von Abhängigkeiten, das Ausführen Ihrer Pfadoperation-Funktion, das Serialisieren der Response und das Ausführen jedes Tasks in FastAPIs `BackgroundTasks`. Diese Spans verwenden denselben Provider und dieselben Exporter.
Die Spans der Hintergrundtasks bleiben Teil des Request-Traces. Sie werden ausgeführt, nachdem der HTTP-Response-Span beendet ist, und erhöhen daher nicht die gemessene Responsezeit.
Um nur den HTTP-Request-Span aufzuzeichnen, setzen Sie `operation_spans` auf `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### WebSocket-Verbindungen nachverfolgen { #trace-websocket-connections }
Jede WebSocket-Verbindung hat einen Span wie `WS /ws/{room}`, der den Handler und das Aufräumen der Abhängigkeiten umfasst. Er verwendet dieselben Provider und Einstellungen, einschließlich `operation_spans` für das Auflösen von Abhängigkeiten und das Ausführen des Endpunkts.
HTTP-Request-Metriken erfassen nur HTTP-Requests. Normale WebSocket-Verbindungsabbrüche mit den Codes `1000` oder `1001` erzeugen keine Fehlerlogs.
### Fehler untersuchen { #inspect-errors }
FastAPI zeichnet unbehandelte Exceptions als OpenTelemetry-Logs auf, die mit dem Trace des Requests oder der Verbindung verknüpft sind. Fehlerlogs werden auch dann aufgezeichnet, wenn der Trace nicht durch Sampling erfasst wird.
Exception-Logs enthalten den Typ, die Nachricht und den Stacktrace der Exception. Nachrichten und Stacktraces können sensible Informationen enthalten. Verwenden Sie die Logprozessoren Ihres Providers, um diese zu filtern oder zu schwärzen, oder setzen Sie `logs` auf `False`, um diese Logs zu deaktivieren.
FastAPI zeichnet außerdem fehlgeschlagene Request-Validierungen als Warnungslogs mit der Route und der Fehleranzahl auf. Diese Logs enthalten nicht die ungültigen Eingabedaten.
## Die aufzuzeichnenden Daten auswählen { #choose-what-to-record }
Das Dictionary `telemetry` akzeptiert außerdem diese Einstellungen:
| Einstellung | Zweck | Defaultwert |
| --- | --- | --- |
| `tracing` | Spans für HTTP-Requests und WebSocket-Verbindungen aufzeichnen | `True` |
| `metrics` | HTTP-Request-Metriken aufzeichnen | `True` |
| `logs` | Fehlgeschlagene Validierungen und unbehandelte Exceptions aufzeichnen | `True` |
| `operation_spans` | Spans für Request-Operationen hinzufügen | `True` |
| `exclude` | Requests überspringen, wenn eine Funktion, die den ASGI-Scope empfängt, `True` zurückgibt | `None` |
| `auto_configure` | Exporter für Endpunkte hinzufügen, die in Umgebungsvariablen gesetzt sind | `True` |
Um beispielsweise Metriken zu erfassen und dabei Healthchecks auszuschließen:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Setzen Sie `auto_configure` auf `False`, wenn Ihre Anwendung die Einrichtung der Provider selbst übernimmt, etwa innerhalb ihrer Lifespan-Funktion.
+24 -51
View File
@@ -1,8 +1,5 @@
sponsors:
- - login: renderinc
avatarUrl: https://avatars.githubusercontent.com/u/36424661?v=4
url: https://github.com/renderinc
- login: subtotal
- - login: subtotal
avatarUrl: https://avatars.githubusercontent.com/u/176449348?v=4
url: https://github.com/subtotal
- login: greptileai
@@ -29,9 +26,6 @@ sponsors:
- login: svix
avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4
url: https://github.com/svix
- login: permitio
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
url: https://github.com/permitio
- login: databento
avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4
url: https://github.com/databento
@@ -50,16 +44,16 @@ sponsors:
- - login: manulife-ai
avatarUrl: https://avatars.githubusercontent.com/u/195145621?v=4
url: https://github.com/manulife-ai
- login: ptimizeroracle
avatarUrl: https://avatars.githubusercontent.com/u/99191972?u=ee268f7afba9856cbd30859ba33708849bc2ece4&v=4
url: https://github.com/ptimizeroracle
- login: scalar
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
url: https://github.com/scalar
- login: Trivie
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie
- - login: takashi-yoneya
avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4
url: https://github.com/takashi-yoneya
- login: Doist
- - login: Doist
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
url: https://github.com/Doist
- - login: alixlahuec
@@ -74,9 +68,6 @@ sponsors:
- login: ChargeStorm
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
url: https://github.com/ChargeStorm
- login: tltaylor1
avatarUrl: https://avatars.githubusercontent.com/u/234697691?u=f9bf860a7a6e1109b35f1eb930920bd37e3bf571&v=4
url: https://github.com/tltaylor1
- login: justoutofcuriosity
avatarUrl: https://avatars.githubusercontent.com/u/57193655?v=4
url: https://github.com/justoutofcuriosity
@@ -107,6 +98,9 @@ sponsors:
- login: ashi-agrawal
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
url: https://github.com/ashi-agrawal
- login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey
- login: jugeeem
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=e4df530e99a086a1085f3dc125b94783670fb383&v=4
url: https://github.com/jugeeem
@@ -125,30 +119,18 @@ sponsors:
- login: anthonycepeda
avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4
url: https://github.com/anthonycepeda
- login: PedroRuizCode
avatarUrl: https://avatars.githubusercontent.com/u/69810923?u=26f9b262922b724deacdfa16ccd5785d093ac371&v=4
url: https://github.com/PedroRuizCode
- login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia
- login: bradsward
avatarUrl: https://avatars.githubusercontent.com/u/60303274?u=6b74120e142c8bd4dae22edcf3ac6bb9c3b427bb&v=4
url: https://github.com/bradsward
- login: dudikbender
avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4
url: https://github.com/dudikbender
- login: jaredtrog
avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4
url: https://github.com/jaredtrog
- login: Ryandaydev
avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=87b1afc7f4fff933779959270e2d168339e91402&v=4
url: https://github.com/Ryandaydev
- login: gorhack
avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4
url: https://github.com/gorhack
- login: mj0331
avatarUrl: https://avatars.githubusercontent.com/u/3890353?u=1c627ac1a024515b4871de5c3ebbfaa1a57f65d4&v=4
url: https://github.com/mj0331
- login: aacayaco
avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4
url: https://github.com/aacayaco
@@ -170,6 +152,9 @@ sponsors:
- login: netsatan
avatarUrl: https://avatars.githubusercontent.com/u/955557?u=cb8fc0ae7f7b06807f0a58e335b1af96c9da0344&v=4
url: https://github.com/netsatan
- login: KevinTCoughlin
avatarUrl: https://avatars.githubusercontent.com/u/706967?u=f00384d8ecf138ebb1f7d6aca34dc2a9d5431dc1&v=4
url: https://github.com/KevinTCoughlin
- login: koxudaxi
avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4
url: https://github.com/koxudaxi
@@ -179,12 +164,6 @@ sponsors:
- login: jstanden
avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
url: https://github.com/jstanden
- login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey
- login: cizekmilan
avatarUrl: https://avatars.githubusercontent.com/u/15999191?u=df194a9bf279f503bc96af41a35c0027eb94ad4f&v=4
url: https://github.com/cizekmilan
- login: khadrawy
avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4
url: https://github.com/khadrawy
@@ -194,9 +173,6 @@ sponsors:
- login: jsoques
avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4
url: https://github.com/jsoques
- login: wdwinslow
avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4
url: https://github.com/wdwinslow
- login: hiancdtrsnm
avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4
url: https://github.com/hiancdtrsnm
@@ -215,13 +191,13 @@ sponsors:
- login: ternaus
avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4
url: https://github.com/ternaus
- login: jaredtrog
avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4
url: https://github.com/jaredtrog
- - login: jpfyoder
avatarUrl: https://avatars.githubusercontent.com/u/7548821?u=1683290ed65dae6987d673da044067577ee71521&v=4
url: https://github.com/jpfyoder
- - login: manoelpqueiroz
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
url: https://github.com/manoelpqueiroz
- login: Artur-Galstyan
- - login: Artur-Galstyan
avatarUrl: https://avatars.githubusercontent.com/u/63471891?u=e8691f386037e51a737cc0ba866cd8c89e5cf109&v=4
url: https://github.com/Artur-Galstyan
- - login: pawamoy
@@ -251,9 +227,6 @@ sponsors:
- login: joshuatz
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
url: https://github.com/joshuatz
- login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood
- login: my3
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
url: https://github.com/my3
@@ -305,12 +278,12 @@ sponsors:
- login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota
- login: rangulvers
avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
url: https://github.com/rangulvers
- - login: mohammadi-hadi
avatarUrl: https://avatars.githubusercontent.com/u/50410241?u=1137b5ff9ea8585c0192fe9ddb4ae5e21bc3d2e8&v=4
url: https://github.com/mohammadi-hadi
- login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood
- - login: emrahbatigun
avatarUrl: https://avatars.githubusercontent.com/u/52425144?u=ddf9a50b6b5e9d32f0bb50650988ef64b09646e8&v=4
url: https://github.com/emrahbatigun
- login: morzan1001
avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
url: https://github.com/morzan1001
@@ -332,12 +305,12 @@ sponsors:
- login: diogotoporcov
avatarUrl: https://avatars.githubusercontent.com/u/207575398?u=1fa7cf41b4181faa4d27f38bc37a374c17b5163b&v=4
url: https://github.com/diogotoporcov
- login: anandakrishnone
avatarUrl: https://avatars.githubusercontent.com/u/100110721?u=5d30e6fea1524bf3c8be3547027dbce59cc699af&v=4
url: https://github.com/anandakrishnone
- login: onestn
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
url: https://github.com/onestn
- login: gabe-santana
avatarUrl: https://avatars.githubusercontent.com/u/59267719?u=84b3472a9f82a7ccdf6fbd6788a12661990b66f4&v=4
url: https://github.com/gabe-santana
- login: Toothwitch
avatarUrl: https://avatars.githubusercontent.com/u/1710406?u=5eebb23b46cd26e48643b9e5179536cad491c17a&v=4
url: https://github.com/Toothwitch
-7
View File
@@ -12,10 +12,6 @@ gold:
img: /img/sponsors/propelauth.png
banner_url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=topbanner
banner_img: /img/sponsors/propelauth-banner.png
- url: https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi
title: Deploy & scale any full-stack web app on Render. Focus on building apps, not infra.
img: /img/sponsors/render.svg
banner_img: /img/sponsors/render-banner.svg
- url: https://www.coderabbit.ai/?utm_source=fastapi&utm_medium=badge&utm_campaign=fastapi
title: Cut Code Review Time & Bugs in Half with CodeRabbit
img: /img/sponsors/coderabbit.png
@@ -45,9 +41,6 @@ silver:
- url: https://www.svix.com/
title: Svix - Webhooks as a service
img: /img/sponsors/svix.svg
- url: https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi
title: Fine-Grained Authorization for FastAPI
img: /img/sponsors/permit.png
- url: https://dribia.com/en/
title: Dribia - Data Science within your reach
img: /img/sponsors/dribia.png
+191 -191
View File
@@ -1,242 +1,232 @@
repos:
- name: headroom
html_url: https://github.com/headroomlabs-ai/headroom
stars: 68267
stars: 74217
owner_login: headroomlabs-ai
owner_html_url: https://github.com/headroomlabs-ai
- name: full-stack-fastapi-template
html_url: https://github.com/fastapi/full-stack-fastapi-template
stars: 45272
stars: 45830
owner_login: fastapi
owner_html_url: https://github.com/fastapi
- name: Hello-Python
html_url: https://github.com/mouredev/Hello-Python
stars: 37219
stars: 37617
owner_login: mouredev
owner_html_url: https://github.com/mouredev
- name: serve
html_url: https://github.com/jina-ai/serve
stars: 21860
stars: 21864
owner_login: jina-ai
owner_html_url: https://github.com/jina-ai
- name: HivisionIDPhotos
html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos
stars: 21468
stars: 21582
owner_login: Zeyi-Lin
owner_html_url: https://github.com/Zeyi-Lin
- name: Douyin_TikTok_Download_API
html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API
stars: 19776
stars: 20422
owner_login: Evil0ctal
owner_html_url: https://github.com/Evil0ctal
- name: sqlmodel
html_url: https://github.com/fastapi/sqlmodel
stars: 18295
stars: 18354
owner_login: fastapi
owner_html_url: https://github.com/fastapi
- name: fastapi-best-practices
html_url: https://github.com/zhanymkanov/fastapi-best-practices
stars: 18006
stars: 18138
owner_login: zhanymkanov
owner_html_url: https://github.com/zhanymkanov
- name: SurfSense
html_url: https://github.com/MODSetter/SurfSense
stars: 16046
owner_login: MODSetter
owner_html_url: https://github.com/MODSetter
- name: machine-learning-zoomcamp
html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp
stars: 14069
stars: 14648
owner_login: DataTalksClub
owner_html_url: https://github.com/DataTalksClub
- name: XHS-Downloader
html_url: https://github.com/JoeanAmier/XHS-Downloader
stars: 12563
stars: 12882
owner_login: JoeanAmier
owner_html_url: https://github.com/JoeanAmier
- name: fastapi_mcp
html_url: https://github.com/tadata-org/fastapi_mcp
stars: 11994
stars: 12014
owner_login: tadata-org
owner_html_url: https://github.com/tadata-org
- name: peewee
html_url: https://github.com/coleifer/peewee
stars: 11986
stars: 11997
owner_login: coleifer
owner_html_url: https://github.com/coleifer
- name: awesome-fastapi
html_url: https://github.com/mjhea0/awesome-fastapi
stars: 11633
stars: 11696
owner_login: mjhea0
owner_html_url: https://github.com/mjhea0
- name: WhisperLiveKit
html_url: https://github.com/QuentinFuxa/WhisperLiveKit
stars: 10983
stars: 11108
owner_login: QuentinFuxa
owner_html_url: https://github.com/QuentinFuxa
- name: polar
html_url: https://github.com/polarsource/polar
stars: 10225
stars: 10300
owner_login: polarsource
owner_html_url: https://github.com/polarsource
- name: pycaret
html_url: https://github.com/pycaret/pycaret
stars: 9833
stars: 9849
owner_login: pycaret
owner_html_url: https://github.com/pycaret
- name: FastUI
html_url: https://github.com/pydantic/FastUI
stars: 8953
stars: 8940
owner_login: pydantic
owner_html_url: https://github.com/pydantic
- name: FileCodeBox
html_url: https://github.com/vastsa/FileCodeBox
stars: 8498
stars: 8559
owner_login: vastsa
owner_html_url: https://github.com/vastsa
- name: hatchet
html_url: https://github.com/hatchet-dev/hatchet
stars: 7824
stars: 8039
owner_login: hatchet-dev
owner_html_url: https://github.com/hatchet-dev
- name: nonebot2
html_url: https://github.com/nonebot/nonebot2
stars: 7691
stars: 7727
owner_login: nonebot
owner_html_url: https://github.com/nonebot
- name: honcho
html_url: https://github.com/plastic-labs/honcho
stars: 6974
owner_login: plastic-labs
owner_html_url: https://github.com/plastic-labs
- name: Yuxi
html_url: https://github.com/xerrors/Yuxi
stars: 6604
stars: 7251
owner_login: xerrors
owner_html_url: https://github.com/xerrors
- name: dramaclaw
html_url: https://github.com/dramaclaw/dramaclaw
stars: 6615
owner_login: dramaclaw
owner_html_url: https://github.com/dramaclaw
- name: fastapi-users
html_url: https://github.com/fastapi-users/fastapi-users
stars: 6236
stars: 6248
owner_login: fastapi-users
owner_html_url: https://github.com/fastapi-users
- name: serge
html_url: https://github.com/serge-chat/serge
stars: 5712
stars: 5708
owner_login: serge-chat
owner_html_url: https://github.com/serge-chat
- name: YouDub-webui
html_url: https://github.com/liuzhao1225/YouDub-webui
stars: 5401
stars: 5563
owner_login: liuzhao1225
owner_html_url: https://github.com/liuzhao1225
- name: Kokoro-FastAPI
html_url: https://github.com/remsky/Kokoro-FastAPI
stars: 5392
stars: 5499
owner_login: remsky
owner_html_url: https://github.com/remsky
- name: dramaclaw
html_url: https://github.com/dramaclaw/dramaclaw
stars: 4892
owner_login: dramaclaw
owner_html_url: https://github.com/dramaclaw
- name: OpenExecutive
html_url: https://github.com/SenteLabsAI/OpenExecutive
stars: 5434
owner_login: SenteLabsAI
owner_html_url: https://github.com/SenteLabsAI
- name: tick-stock-panel
html_url: https://github.com/shy3130/tick-stock-panel
stars: 5353
owner_login: shy3130
owner_html_url: https://github.com/shy3130
- name: devpush
html_url: https://github.com/hunvreus/devpush
stars: 4747
stars: 4758
owner_login: hunvreus
owner_html_url: https://github.com/hunvreus
- name: strawberry
html_url: https://github.com/strawberry-graphql/strawberry
stars: 4710
stars: 4722
owner_login: strawberry-graphql
owner_html_url: https://github.com/strawberry-graphql
- name: mcp-context-forge
html_url: https://github.com/IBM/mcp-context-forge
stars: 4557
owner_login: IBM
owner_html_url: https://github.com/IBM
- name: logfire
html_url: https://github.com/pydantic/logfire
stars: 4449
stars: 4500
owner_login: pydantic
owner_html_url: https://github.com/pydantic
- name: poem
html_url: https://github.com/poem-web/poem
stars: 4438
stars: 4443
owner_login: poem-web
owner_html_url: https://github.com/poem-web
- name: mcp-context-forge
html_url: https://github.com/IBM/mcp-context-forge
stars: 4400
owner_login: IBM
owner_html_url: https://github.com/IBM
- name: huma
html_url: https://github.com/danielgtaylor/huma
stars: 4365
stars: 4433
owner_login: danielgtaylor
owner_html_url: https://github.com/danielgtaylor
- name: dynaconf
html_url: https://github.com/dynaconf/dynaconf
stars: 4325
stars: 4333
owner_login: dynaconf
owner_html_url: https://github.com/dynaconf
- name: chatgpt-web-share
html_url: https://github.com/chatpire/chatgpt-web-share
stars: 4273
stars: 4276
owner_login: chatpire
owner_html_url: https://github.com/chatpire
- name: tick-stock-panel
html_url: https://github.com/shy3130/tick-stock-panel
stars: 4101
owner_login: shy3130
owner_html_url: https://github.com/shy3130
- name: RVG
html_url: https://github.com/arvin341az-glitch/RVG
stars: 4140
owner_login: arvin341az-glitch
owner_html_url: https://github.com/arvin341az-glitch
- name: atrilabs-engine
html_url: https://github.com/Atri-Labs/atrilabs-engine
stars: 4066
stars: 4063
owner_login: Atri-Labs
owner_html_url: https://github.com/Atri-Labs
- name: datamodel-code-generator
html_url: https://github.com/koxudaxi/datamodel-code-generator
stars: 4008
owner_login: koxudaxi
owner_html_url: https://github.com/koxudaxi
html_url: https://github.com/datamodel-code-generator/datamodel-code-generator
stars: 4027
owner_login: datamodel-code-generator
owner_html_url: https://github.com/datamodel-code-generator
- name: LitServe
html_url: https://github.com/Lightning-AI/LitServe
stars: 3934
stars: 3943
owner_login: Lightning-AI
owner_html_url: https://github.com/Lightning-AI
- name: RVG
html_url: https://github.com/arvin341az-glitch/RVG
stars: 3906
owner_login: arvin341az-glitch
owner_html_url: https://github.com/arvin341az-glitch
- name: fastapi-admin
html_url: https://github.com/fastapi-admin/fastapi-admin
stars: 3819
stars: 3828
owner_login: fastapi-admin
owner_html_url: https://github.com/fastapi-admin
- name: tracecat
html_url: https://github.com/TracecatHQ/tracecat
stars: 3782
stars: 3823
owner_login: TracecatHQ
owner_html_url: https://github.com/TracecatHQ
- name: Rapid-MLX
html_url: https://github.com/raullenchai/Rapid-MLX
stars: 3630
owner_login: raullenchai
owner_html_url: https://github.com/raullenchai
- name: farfalle
html_url: https://github.com/rashadphz/farfalle
stars: 3543
stars: 3540
owner_login: rashadphz
owner_html_url: https://github.com/rashadphz
- name: OpenExecutive
html_url: https://github.com/SenteLabsAI/OpenExecutive
stars: 3322
owner_login: SenteLabsAI
owner_html_url: https://github.com/SenteLabsAI
- name: any-auto-register
html_url: https://github.com/lxf746/any-auto-register
stars: 3221
stars: 3322
owner_login: lxf746
owner_html_url: https://github.com/lxf746
- name: codex-lb
html_url: https://github.com/Soju06/codex-lb
stars: 3284
owner_login: Soju06
owner_html_url: https://github.com/Soju06
- name: opyrator
html_url: https://github.com/ml-tooling/opyrator
stars: 3131
stars: 3132
owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling
- name: docarray
@@ -246,72 +236,72 @@ repos:
owner_html_url: https://github.com/docarray
- name: fastapi-realworld-example-app
html_url: https://github.com/nsidnev/fastapi-realworld-example-app
stars: 3107
stars: 3104
owner_login: nsidnev
owner_html_url: https://github.com/nsidnev
- name: uvicorn-gunicorn-fastapi-docker
html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker
stars: 2916
stars: 2914
owner_login: tiangolo
owner_html_url: https://github.com/tiangolo
- name: codex-lb
html_url: https://github.com/Soju06/codex-lb
stars: 2904
owner_login: Soju06
owner_html_url: https://github.com/Soju06
- name: FastAPI-template
html_url: https://github.com/s3rius/FastAPI-template
stars: 2826
owner_login: s3rius
owner_html_url: https://github.com/s3rius
- name: sqladmin
html_url: https://github.com/smithyhq/sqladmin
stars: 2817
stars: 2837
owner_login: smithyhq
owner_html_url: https://github.com/smithyhq
- name: YC-Killer
html_url: https://github.com/sahibzada-allahyar/YC-Killer
stars: 2805
stars: 2834
owner_login: sahibzada-allahyar
owner_html_url: https://github.com/sahibzada-allahyar
- name: FastAPI-template
html_url: https://github.com/s3rius/FastAPI-template
stars: 2833
owner_login: s3rius
owner_html_url: https://github.com/s3rius
- name: NoteDiscovery
html_url: https://github.com/gamosoft/NoteDiscovery
stars: 2776
stars: 2815
owner_login: gamosoft
owner_html_url: https://github.com/gamosoft
- name: best-of-web-python
html_url: https://github.com/ml-tooling/best-of-web-python
stars: 2755
stars: 2763
owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling
- name: fastapi-langgraph-agent-production-ready-template
html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template
stars: 2630
stars: 2686
owner_login: wassim249
owner_html_url: https://github.com/wassim249
- name: fastapi-react
html_url: https://github.com/Buuntu/fastapi-react
stars: 2584
owner_login: Buuntu
owner_html_url: https://github.com/Buuntu
- name: supabase-py
html_url: https://github.com/supabase/supabase-py
stars: 2571
stars: 2596
owner_login: supabase
owner_html_url: https://github.com/supabase
- name: open-wearables
html_url: https://github.com/the-momentum/open-wearables
stars: 2591
owner_login: the-momentum
owner_html_url: https://github.com/the-momentum
- name: fastapi-react
html_url: https://github.com/Buuntu/fastapi-react
stars: 2591
owner_login: Buuntu
owner_html_url: https://github.com/Buuntu
- name: fastapi-best-architecture
html_url: https://github.com/fastapi-practices/fastapi-best-architecture
stars: 2530
stars: 2573
owner_login: fastapi-practices
owner_html_url: https://github.com/fastapi-practices
- name: 30-Days-of-Python
html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python
stars: 2511
stars: 2524
owner_login: codingforentrepreneurs
owner_html_url: https://github.com/codingforentrepreneurs
- name: AIstudioProxyAPI
html_url: https://github.com/CJackHwang/AIstudioProxyAPI
stars: 2496
stars: 2514
owner_login: CJackHwang
owner_html_url: https://github.com/CJackHwang
- name: RasaGPT
@@ -319,178 +309,188 @@ repos:
stars: 2463
owner_login: paulpierre
owner_html_url: https://github.com/paulpierre
- name: open-wearables
html_url: https://github.com/the-momentum/open-wearables
stars: 2432
owner_login: the-momentum
owner_html_url: https://github.com/the-momentum
- name: nextpy
html_url: https://github.com/dot-agent/nextpy
stars: 2349
stars: 2346
owner_login: dot-agent
owner_html_url: https://github.com/dot-agent
- name: langserve
html_url: https://github.com/langchain-ai/langserve
stars: 2329
stars: 2327
owner_login: langchain-ai
owner_html_url: https://github.com/langchain-ai
- name: fastapi-utils
html_url: https://github.com/fastapiutils/fastapi-utils
stars: 2309
stars: 2308
owner_login: fastapiutils
owner_html_url: https://github.com/fastapiutils
- name: kiro-gateway
html_url: https://github.com/jwadow/kiro-gateway
stars: 2251
stars: 2299
owner_login: jwadow
owner_html_url: https://github.com/jwadow
- name: vue-fastapi-admin
html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin
stars: 2245
stars: 2261
owner_login: mizhexiaoxiao
owner_html_url: https://github.com/mizhexiaoxiao
- name: creatorhub
html_url: https://github.com/3441293738/creatorhub
stars: 2200
owner_login: '3441293738'
owner_html_url: https://github.com/3441293738
- name: solara
html_url: https://github.com/widgetti/solara
stars: 2172
stars: 2179
owner_login: widgetti
owner_html_url: https://github.com/widgetti
- name: mangum
html_url: https://github.com/Kludex/mangum
stars: 2131
stars: 2138
owner_login: Kludex
owner_html_url: https://github.com/Kludex
- name: FastAPI-boilerplate
html_url: https://github.com/benavlabs/FastAPI-boilerplate
stars: 2070
stars: 2092
owner_login: benavlabs
owner_html_url: https://github.com/benavlabs
- name: xhs_ai_publisher
html_url: https://github.com/BetaStreetOmnis/xhs_ai_publisher
stars: 2068
stars: 2089
owner_login: BetaStreetOmnis
owner_html_url: https://github.com/BetaStreetOmnis
- name: slowapi
html_url: https://github.com/laurentS/slowapi
stars: 2052
stars: 2067
owner_login: laurentS
owner_html_url: https://github.com/laurentS
- name: openapi-python-client
html_url: https://github.com/openapi-generators/openapi-python-client
stars: 1985
stars: 1995
owner_login: openapi-generators
owner_html_url: https://github.com/openapi-generators
- name: agentkit
html_url: https://github.com/BCG-X-Official/agentkit
stars: 1949
owner_login: BCG-X-Official
owner_html_url: https://github.com/BCG-X-Official
- name: piccolo
html_url: https://github.com/piccolo-orm/piccolo
stars: 1936
stars: 1948
owner_login: piccolo-orm
owner_html_url: https://github.com/piccolo-orm
- name: manage-fastapi
html_url: https://github.com/ycd/manage-fastapi
stars: 1907
owner_login: ycd
owner_html_url: https://github.com/ycd
- name: fastapi-cache
html_url: https://github.com/long2ice/fastapi-cache
stars: 1866
owner_login: long2ice
owner_html_url: https://github.com/long2ice
- name: WebRPA
html_url: https://github.com/pmh1314520/WebRPA
stars: 1866
owner_login: pmh1314520
owner_html_url: https://github.com/pmh1314520
- name: agentkit
html_url: https://github.com/BCG-X-Official/agentkit
stars: 1946
owner_login: BCG-X-Official
owner_html_url: https://github.com/BCG-X-Official
- name: full-stack-ai-agent-template
html_url: https://github.com/vstorm-co/full-stack-ai-agent-template
stars: 1862
stars: 1926
owner_login: vstorm-co
owner_html_url: https://github.com/vstorm-co
- name: creatorhub
html_url: https://github.com/3441293738/creatorhub
- name: PanWatch
html_url: https://github.com/TNT-Likely/PanWatch
stars: 1915
owner_login: TNT-Likely
owner_html_url: https://github.com/TNT-Likely
- name: manage-fastapi
html_url: https://github.com/ycd/manage-fastapi
stars: 1905
owner_login: ycd
owner_html_url: https://github.com/ycd
- name: WebRPA
html_url: https://github.com/pmh1314520/WebRPA
stars: 1899
owner_login: pmh1314520
owner_html_url: https://github.com/pmh1314520
- name: fastapi-cache
html_url: https://github.com/long2ice/fastapi-cache
stars: 1867
owner_login: long2ice
owner_html_url: https://github.com/long2ice
- name: FileSync
html_url: https://github.com/polius/FileSync
stars: 1816
owner_login: '3441293738'
owner_html_url: https://github.com/3441293738
owner_login: polius
owner_html_url: https://github.com/polius
- name: ormar
html_url: https://github.com/ormar-orm/ormar
stars: 1802
stars: 1803
owner_login: ormar-orm
owner_html_url: https://github.com/ormar-orm
- name: python-week-2022
html_url: https://github.com/rochacbruno/python-week-2022
stars: 1797
stars: 1796
owner_login: rochacbruno
owner_html_url: https://github.com/rochacbruno
- name: termpair
html_url: https://github.com/cs01/termpair
stars: 1779
stars: 1778
owner_login: cs01
owner_html_url: https://github.com/cs01
- name: bracket
html_url: https://github.com/evroon/bracket
stars: 1728
stars: 1740
owner_login: evroon
owner_html_url: https://github.com/evroon
- name: fastapi-crudrouter
html_url: https://github.com/awtkns/fastapi-crudrouter
stars: 1696
stars: 1698
owner_login: awtkns
owner_html_url: https://github.com/awtkns
- name: docling-api
html_url: https://github.com/drmingler/docling-api
stars: 1688
owner_login: drmingler
owner_html_url: https://github.com/drmingler
- name: fastapi-pagination
html_url: https://github.com/uriyyo/fastapi-pagination
stars: 1680
stars: 1682
owner_login: uriyyo
owner_html_url: https://github.com/uriyyo
- name: langchain-serve
html_url: https://github.com/jina-ai/langchain-serve
stars: 1641
stars: 1637
owner_login: jina-ai
owner_html_url: https://github.com/jina-ai
- name: awesome-fastapi-projects
html_url: https://github.com/Kludex/awesome-fastapi-projects
stars: 1618
stars: 1620
owner_login: Kludex
owner_html_url: https://github.com/Kludex
- name: docling-api
html_url: https://github.com/drmingler/docling-api
stars: 1597
owner_login: drmingler
owner_html_url: https://github.com/drmingler
- name: fastcrud
html_url: https://github.com/benavlabs/fastcrud
stars: 1577
owner_login: benavlabs
owner_html_url: https://github.com/benavlabs
- name: coronavirus-tracker-api
html_url: https://github.com/ExpDev07/coronavirus-tracker-api
stars: 1569
owner_login: ExpDev07
owner_html_url: https://github.com/ExpDev07
- name: fastapi-amis-admin
html_url: https://github.com/amisadmin/fastapi-amis-admin
stars: 1566
owner_login: amisadmin
owner_html_url: https://github.com/amisadmin
- name: tavily-key-generator
html_url: https://github.com/skernelx/tavily-key-generator
stars: 1553
owner_login: skernelx
owner_html_url: https://github.com/skernelx
- name: yubal
html_url: https://github.com/guillevc/yubal
stars: 1529
stars: 1597
owner_login: guillevc
owner_html_url: https://github.com/guillevc
- name: fastcrud
html_url: https://github.com/benavlabs/fastcrud
stars: 1592
owner_login: benavlabs
owner_html_url: https://github.com/benavlabs
- name: fastapi-amis-admin
html_url: https://github.com/amisadmin/fastapi-amis-admin
stars: 1568
owner_login: amisadmin
owner_html_url: https://github.com/amisadmin
- name: coronavirus-tracker-api
html_url: https://github.com/ExpDev07/coronavirus-tracker-api
stars: 1568
owner_login: ExpDev07
owner_html_url: https://github.com/ExpDev07
- name: tavily-key-generator
html_url: https://github.com/skernelx/tavily-key-generator
stars: 1561
owner_login: skernelx
owner_html_url: https://github.com/skernelx
- name: fim-one
html_url: https://github.com/fim-ai/fim-one
stars: 1550
owner_login: fim-ai
owner_html_url: https://github.com/fim-ai
- name: RuoYi-Vue3-FastAPI
html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI
stars: 1516
stars: 1544
owner_login: insistence
owner_html_url: https://github.com/insistence
- name: FileSync
html_url: https://github.com/polius/FileSync
stars: 1494
owner_login: polius
owner_html_url: https://github.com/polius
- name: fastapi-boilerplate
html_url: https://github.com/teamhide/fastapi-boilerplate
stars: 1495
owner_login: teamhide
owner_html_url: https://github.com/teamhide
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
When your API is running, you might want to know how much traffic it receives, which requests are slow, and when errors happen.
**Telemetry** is data about your application's behavior that helps you answer these questions. Common types include:
- **Metrics**: measurements you can summarize over time, such as response times and the number of requests being handled.
- **Traces**: records of individual requests and the operations performed to handle them. Each timed operation is called a **span**.
- **Logs**: timestamped records of events, such as an application starting or an operation failing.
[**OpenTelemetry**](https://opentelemetry.io/) is a set of standards and tools for collecting telemetry and sending it to a monitoring service, where you can explore it in dashboards.
**FastAPI provides OpenTelemetry support by default** for HTTP request traces, metrics, and logs. WebSocket connections also provide traces and logs. To see that data, configure a monitoring service to receive it.
## Install FastAPI { #install-fastapi }
Install FastAPI with the `standard` extras, which include the packages for sending telemetry:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Create the app { #create-the-app }
Create a file `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Notice that it all works by default, you don't need to write any custom code for telemetry to work.
## FastAPI Cloud { #fastapi-cloud }
When you deploy to [FastAPI Cloud](https://fastapicloud.com) with `fastapi[standard]`, metrics work automatically. You don't have to configure anything else.
On Pro plans, you can view request counts, error rates, and response times in the [Metrics dashboard](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="FastAPI Cloud Pro metrics dashboard with example data">
## Other monitoring services { #other-monitoring-services }
To send telemetry to another monitoring service, configure an endpoint that accepts **OTLP**, the OpenTelemetry protocol for sending telemetry. Use the service's HTTP/protobuf base endpoint.
Set these environment variables, replacing the example URL with your endpoint:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` identifies your app in the monitoring service. The endpoint is the base URL for receiving data. Traces are sent to `/v1/traces`, metrics to `/v1/metrics`, and logs to `/v1/logs` under that URL.
If your service requires authentication, set `OTEL_EXPORTER_OTLP_HEADERS` to the headers it specifies, for example `api-key=YOUR_API_KEY`.
## Run the app { #run-the-app }
Start the app in the same terminal:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
In another terminal, send a request:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Open your monitoring service and find `my-api`. After the next export, you can see a trace with a `GET /items/{item_id}` span, along with metrics for request counts, response duration, and active requests.
## Customize telemetry { #customize-telemetry }
### Configure providers and exporters { #configure-providers-and-exporters }
A **provider** supplies the objects that record traces, metrics, or logs. Its configuration controls how that data is processed and exported.
Telemetry libraries can configure OpenTelemetry's global providers. Configure the library before the app starts, and FastAPI uses those providers automatically.
When an OTLP endpoint is set in the environment, FastAPI adds an exporter for that destination to each enabled provider. Existing exporters continue sending data to their destinations.
Configure each destination once. If another library already handles the environment destination, disable its environment export or turn off FastAPI's automatic setup:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
You can also pass a provider directly in the `telemetry` dictionary. For example, this provider uses OpenTelemetry's console exporter to print request spans in your terminal:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
The **exporter** sends the spans to their destination. `BatchSpanProcessor` groups spans and sends them in the background. Replace the console exporter with one supplied by your monitoring library to use its destination. See [OpenTelemetry's Python instrumentation guide](https://opentelemetry.io/docs/languages/python/instrumentation/) for more configuration options.
Use `meter_provider` or `logger_provider` in the same dictionary to supply a metrics or logs provider. The application or library creating a provider manages its shutdown. FastAPI manages the export components it adds.
/// warning
OpenTelemetry uses global providers by default. Independent telemetry configuration for [mounted sub-applications](sub-applications.md) is not guaranteed.
///
### Trace request operations { #trace-request-operations }
By default, request traces include spans for resolving dependencies, running your path operation function, serializing the response, and running each task in FastAPI's `BackgroundTasks`. These spans use the same provider and exporters.
Background task spans remain part of the request's trace. They run after the HTTP response span ends, so they do not increase the measured response time.
To record only the HTTP request span, set `operation_spans` to `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Trace WebSocket connections { #trace-websocket-connections }
Each WebSocket connection has a span such as `WS /ws/{room}`, covering the handler and dependency cleanup. It uses the same providers and settings, including `operation_spans` for dependency resolution and endpoint execution.
HTTP request metrics cover HTTP requests only. Normal WebSocket disconnects with codes `1000` or `1001` do not produce error logs.
### Inspect errors { #inspect-errors }
FastAPI records unhandled exceptions as OpenTelemetry logs, linked to the request's or connection's trace. Error logs are recorded even when the trace is not sampled.
Exception logs include the exception's type, message, and stack trace. Messages and stack traces can contain sensitive information. Use your provider's log processors to filter or redact them, or set `logs` to `False` to disable these logs.
FastAPI also records request validation failures as warning logs with the route and error count. These logs do not include the invalid input.
## Choose what to record { #choose-what-to-record }
The `telemetry` dictionary also accepts these settings:
| Setting | Purpose | Default |
| --- | --- | --- |
| `tracing` | Record HTTP request and WebSocket connection spans | `True` |
| `metrics` | Record HTTP request metrics | `True` |
| `logs` | Record validation failures and unhandled exceptions | `True` |
| `operation_spans` | Add spans for request operations | `True` |
| `exclude` | Skip requests when a function receiving the ASGI scope returns `True` | `None` |
| `auto_configure` | Add exporters for endpoints set in environment variables | `True` |
For example, to collect metrics while excluding health checks:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Set `auto_configure` to `False` when your application handles provider setup itself, such as inside its lifespan function.
+7
View File
@@ -549,3 +549,10 @@ Inspired by Termynal's CSS tricks with modifications
/* Hidden in MkDocs; rendered on GitHub (which doesn't load this stylesheet) */
.only-github { display: none; }
.newsletter-embed {
display: block;
width: calc(100% + 8px);
margin: -4px;
border: 0;
}
-1
View File
@@ -20,5 +20,4 @@ Some other cloud providers ✨ [**sponsor FastAPI**](https://github.com/sponsors
You might also want to consider them to follow their guides and try their services:
* [Render](https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi)
* [Railway](https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi)
Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

+33
View File
@@ -238,12 +238,45 @@ function setupOpinionsTabs() {
});
}
let cleanupNewsletterEmbed = () => {};
function setupNewsletterEmbed() {
cleanupNewsletterEmbed();
const frame = document.querySelector('.newsletter-embed');
if (!frame) return;
const origin = new URL(frame.src).origin;
const configure = () => frame.contentWindow.postMessage({
type: 'fastapi-newsletter:configure',
theme: document.body.dataset.mdColorScheme === 'slate' ? 'dark' : 'light',
fontSize: parseFloat(getComputedStyle(frame.parentElement).fontSize),
}, origin);
const resize = (event) => {
if (event.origin !== origin || event.source !== frame.contentWindow) return;
const { type, height } = event.data ?? {};
if (type !== 'fastapi-newsletter:resize' || !Number.isFinite(height) || height <= 0 || height > 2000) return;
frame.style.height = `${Math.ceil(height)}px`;
};
window.addEventListener('message', resize);
frame.addEventListener('load', configure);
window.addEventListener('resize', configure);
const themeObserver = new MutationObserver(configure);
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
configure();
cleanupNewsletterEmbed = () => {
window.removeEventListener('message', resize);
frame.removeEventListener('load', configure);
window.removeEventListener('resize', configure);
themeObserver.disconnect();
};
}
async function main() {
setupTermynal();
showRandomAnnouncement('announce-left', 5000)
handleSponsorImages();
openLinksInNewTab();
setupOpinionsTabs();
setupNewsletterEmbed();
}
document$.subscribe(() => {
main()
+2 -2
View File
@@ -1,5 +1,5 @@
# FastAPI and friends newsletter
<iframe data-w-type="embedded" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://xr4n4.mjt.lu/wgt/xr4n4/hj5/form?c=40a44fa4" width="100%" style="height: 800px;"></iframe>
<iframe class="newsletter-embed" src="https://fastapiandfriends.com/embed/" width="100%" height="280"></iframe>
<script type="text/javascript" src="https://app.mailjet.com/pas-nc-embedded-v1.js"></script>
[Read previous issues](https://fastapiandfriends.com/newsletter) or [subscribe on the FastAPI and friends website](https://fastapiandfriends.com/#subscribe).
+58
View File
@@ -7,6 +7,61 @@ hide:
## Latest Changes
### Docs
* 📝 Remove HTML title from newsletter to avoid the tooltip. PR [#16461](https://github.com/fastapi/fastapi/pull/16461) by [@tiangolo](https://github.com/tiangolo).
* 📝 Embed the FastAPI and friends newsletter signup form. PR [#16459](https://github.com/fastapi/fastapi/pull/16459) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update skill, use Asyncer for blocking code in threads. PR [#16420](https://github.com/fastapi/fastapi/pull/16420) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🌐 Update translations for fr (add-missing). PR [#16436](https://github.com/fastapi/fastapi/pull/16436) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for hi (add-missing). PR [#16435](https://github.com/fastapi/fastapi/pull/16435) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for tr (add-missing). PR [#16434](https://github.com/fastapi/fastapi/pull/16434) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for zh (add-missing). PR [#16433](https://github.com/fastapi/fastapi/pull/16433) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for zh-hant (add-missing). PR [#16432](https://github.com/fastapi/fastapi/pull/16432) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for pt (add-missing). PR [#16431](https://github.com/fastapi/fastapi/pull/16431) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for de (add-missing). PR [#16430](https://github.com/fastapi/fastapi/pull/16430) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for uk (add-missing). PR [#16429](https://github.com/fastapi/fastapi/pull/16429) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for ja (add-missing). PR [#16428](https://github.com/fastapi/fastapi/pull/16428) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for ko (add-missing). PR [#16425](https://github.com/fastapi/fastapi/pull/16425) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for es (add-missing). PR [#16426](https://github.com/fastapi/fastapi/pull/16426) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update translations for ru (add-missing). PR [#16427](https://github.com/fastapi/fastapi/pull/16427) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 🌐 Update Russian LLM-prompt. PR [#16449](https://github.com/fastapi/fastapi/pull/16449) by [@YuriiMotov](https://github.com/YuriiMotov).
### Internal
* 👷 Fix deprecated command in `bump-pre-commit-hooks` workflow. PR [#16463](https://github.com/fastapi/fastapi/pull/16463) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔧 Update sponsors: remove Render. PR [#16457](https://github.com/fastapi/fastapi/pull/16457) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Use `gpt-6-astra` model for translations. PR [#16453](https://github.com/fastapi/fastapi/pull/16453) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump the python-packages group across 1 directory with 14 updates. PR [#16447](https://github.com/fastapi/fastapi/pull/16447) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump starlette from 1.6.0 to 1.7.0. PR [#16440](https://github.com/fastapi/fastapi/pull/16440) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pyjwt from 2.13.0 to 2.15.0. PR [#16423](https://github.com/fastapi/fastapi/pull/16423) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump gitpython from 3.1.60 to 3.1.62. PR [#16444](https://github.com/fastapi/fastapi/pull/16444) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👥 Update FastAPI GitHub topic repositories. PR [#16443](https://github.com/fastapi/fastapi/pull/16443) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* 👥 Update FastAPI People - Sponsors. PR [#16437](https://github.com/fastapi/fastapi/pull/16437) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* ⬆ Bump pre-commit hooks. PR [#16442](https://github.com/fastapi/fastapi/pull/16442) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* ⬆ Bump urllib3 from 2.7.0 to 2.8.0. PR [#16445](https://github.com/fastapi/fastapi/pull/16445) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the github-actions group with 3 updates. PR [#16438](https://github.com/fastapi/fastapi/pull/16438) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.142.2 (2026-09-30)
### Fixes
* 🐛 Allow startup when automatic OpenTelemetry configuration fails. PR [#16418](https://github.com/fastapi/fastapi/pull/16418) by [@tiangolo](https://github.com/tiangolo).
## 0.142.1 (2026-09-29)
### Fixes
* 🐛 Fix repeated endpoint wrapping in included routers. PR [#16414](https://github.com/fastapi/fastapi/pull/16414) by [@tiangolo](https://github.com/tiangolo).
## 0.142.0 (2026-09-29)
### Features
* ✨ Add native OpenTelemetry support. PR [#16403](https://github.com/fastapi/fastapi/pull/16403) by [@tiangolo](https://github.com/tiangolo).
### Refactors
* 📱 Improve mobile responsiveness of conference rail. PR [#16196](https://github.com/fastapi/fastapi/pull/16196) by [@alejsdev](https://github.com/alejsdev).
@@ -35,6 +90,9 @@ hide:
### Internal
* ✅ Fix frontend test timeout with Starlette Git. PR [#16408](https://github.com/fastapi/fastapi/pull/16408) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔧 Update sponsors: remove Permit.io. PR [#16406](https://github.com/fastapi/fastapi/pull/16406) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump anyio from 4.12.1 to 4.14.2. PR [#16375](https://github.com/fastapi/fastapi/pull/16375) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the python-packages group across 1 directory with 15 updates. PR [#16285](https://github.com/fastapi/fastapi/pull/16285) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump starlette from 1.3.1 to 1.6.0. PR [#16289](https://github.com/fastapi/fastapi/pull/16289) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump annotated-doc from 0.0.4 to 0.0.5. PR [#16288](https://github.com/fastapi/fastapi/pull/16288) by [@dependabot[bot]](https://github.com/apps/dependabot).
+1
View File
@@ -159,6 +159,7 @@ nav:
- advanced/templates.md
- advanced/websockets.md
- advanced/events.md
- advanced/opentelemetry.md
- advanced/testing-websockets.md
- advanced/testing-events.md
- advanced/testing-dependencies.md
@@ -10,12 +10,6 @@
<img class="sponsor-image" src="/img/sponsors/propelauth-banner.png" alt="Auth, user management and more for your B2B product" />
</a>
</div>
<div class="item">
<a title="Deploy & scale any full-stack web app on Render. Focus on building apps, not infra." style="display: block; position: relative;" href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/render-banner.svg" alt="Deploy & scale any full-stack web app on Render. Focus on building apps, not infra." />
</a>
</div>
<div class="item">
<a title="Cut Code Review Time & Bugs in Half with CodeRabbit" style="display: block; position: relative;" href="https://www.coderabbit.ai/?utm_source=fastapi&utm_medium=banner&utm_campaign=fastapi" target="_blank">
<span class="sponsor-badge">sponsor</span>
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Cuando tu API está en funcionamiento, puede que quieras saber cuánto tráfico recibe, qué requests son lentas y cuándo se producen errores.
La **telemetría** son datos sobre el comportamiento de tu aplicación que te ayudan a responder estas preguntas. Algunos tipos comunes son:
- **Métricas**: mediciones que puedes resumir a lo largo del tiempo, como los tiempos de response y el número de requests que se están procesando.
- **Trazas**: registros de requests individuales y de las operaciones realizadas para procesarlas. Cada operación cronometrada se llama **span**.
- **Logs**: registros de eventos con marcas de tiempo, como el inicio de una aplicación o el fallo de una operación.
[**OpenTelemetry**](https://opentelemetry.io/) es un conjunto de estándares y herramientas para recopilar telemetría y enviarla a un servicio de monitorización, donde puedes explorarla en paneles.
**FastAPI proporciona soporte para OpenTelemetry por defecto** para trazas, métricas y logs de requests HTTP. Las conexiones WebSocket también proporcionan trazas y logs. Para ver esos datos, configura un servicio de monitorización que los reciba.
## Instala FastAPI { #install-fastapi }
Instala FastAPI con los extras `standard`, que incluyen los paquetes para enviar telemetría:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Crea la aplicación { #create-the-app }
Crea un archivo `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Ten en cuenta que todo funciona por defecto, no necesitas escribir ningún código personalizado para que funcione la telemetría.
## FastAPI Cloud { #fastapi-cloud }
Cuando despliegas en [FastAPI Cloud](https://fastapicloud.com) con `fastapi[standard]`, las métricas funcionan automáticamente. No tienes que configurar nada más.
En los planes Pro, puedes ver el número de requests, las tasas de error y los tiempos de response en el [panel de métricas](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="Panel de métricas de FastAPI Cloud Pro con datos de ejemplo">
## Otros servicios de monitorización { #other-monitoring-services }
Para enviar telemetría a otro servicio de monitorización, configura un endpoint que acepte **OTLP**, el protocolo de OpenTelemetry para enviar telemetría. Usa el endpoint base HTTP/protobuf del servicio.
Establece estas variables de entorno, reemplazando la URL de ejemplo por tu endpoint:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` identifica tu aplicación en el servicio de monitorización. El endpoint es la URL base para recibir datos. Las trazas se envían a `/v1/traces`, las métricas a `/v1/metrics` y los logs a `/v1/logs` bajo esa URL.
Si tu servicio requiere autenticación, establece `OTEL_EXPORTER_OTLP_HEADERS` con los headers que especifique, por ejemplo `api-key=YOUR_API_KEY`.
## Ejecuta la aplicación { #run-the-app }
Inicia la aplicación en la misma terminal:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
En otra terminal, envía una request:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Abre tu servicio de monitorización y busca `my-api`. Después de la siguiente exportación, podrás ver una traza con un span `GET /items/{item_id}`, junto con métricas del número de requests, la duración de las responses y las requests activas.
## Personaliza la telemetría { #customize-telemetry }
### Configura proveedores y exportadores { #configure-providers-and-exporters }
Un **proveedor** proporciona los objetos que registran trazas, métricas o logs. Su configuración controla cómo se procesan y exportan esos datos.
Los paquetes de telemetría pueden configurar los proveedores globales de OpenTelemetry. Configura el paquete antes de que se inicie la aplicación y FastAPI usará esos proveedores automáticamente.
Cuando se establece un endpoint OTLP en el entorno, FastAPI añade un exportador para ese destino a cada proveedor habilitado. Los exportadores existentes siguen enviando datos a sus destinos.
Configura cada destino una sola vez. Si otro paquete ya gestiona el destino del entorno, desactiva su exportación al destino del entorno o desactiva la configuración automática de FastAPI:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
También puedes pasar un proveedor directamente en el diccionario `telemetry`. Por ejemplo, este proveedor usa el exportador de consola de OpenTelemetry para imprimir los spans de las requests en tu terminal:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
El **exportador** envía los spans a su destino. `BatchSpanProcessor` agrupa los spans y los envía en segundo plano. Reemplaza el exportador de consola por uno proporcionado por tu paquete de monitorización para usar su destino. Consulta la [guía de instrumentación de Python de OpenTelemetry](https://opentelemetry.io/docs/languages/python/instrumentation/) para ver más opciones de configuración.
Usa `meter_provider` o `logger_provider` en el mismo diccionario para proporcionar un proveedor de métricas o logs. La aplicación o el paquete que crea un proveedor gestiona su cierre. FastAPI gestiona los componentes de exportación que añade.
/// warning | Advertencia
OpenTelemetry usa proveedores globales por defecto. No se garantiza una configuración de telemetría independiente para las [subaplicaciones montadas](sub-applications.md).
///
### Traza las operaciones de las requests { #trace-request-operations }
Por defecto, las trazas de las requests incluyen spans para resolver dependencias, ejecutar tu path operation function, serializar la response y ejecutar cada tarea de `BackgroundTasks` de FastAPI. Estos spans usan el mismo proveedor y los mismos exportadores.
Los spans de las tareas en segundo plano siguen formando parte de la traza de la request. Se ejecutan después de que termine el span de la response HTTP, por lo que no aumentan el tiempo de response medido.
Para registrar solo el span de la request HTTP, establece `operation_spans` en `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Traza las conexiones WebSocket { #trace-websocket-connections }
Cada conexión WebSocket tiene un span como `WS /ws/{room}`, que abarca el manejador y la limpieza de las dependencias. Usa los mismos proveedores y ajustes, incluido `operation_spans` para la resolución de dependencias y la ejecución del endpoint.
Las métricas de requests HTTP abarcan únicamente requests HTTP. Las desconexiones normales de WebSocket con los códigos `1000` o `1001` no generan logs de error.
### Inspecciona los errores { #inspect-errors }
FastAPI registra las excepciones no controladas como logs de OpenTelemetry, vinculados a la traza de la request o de la conexión. Los logs de error se registran incluso cuando la traza no se incluye en el muestreo.
Los logs de excepciones incluyen el tipo, el mensaje y la traza de la pila de la excepción. Los mensajes y las trazas de la pila pueden contener información sensible. Usa los procesadores de logs de tu proveedor para filtrarlos u ocultar la información sensible, o establece `logs` en `False` para desactivar estos logs.
FastAPI también registra los fallos de validación de las requests como logs de advertencia con la ruta y el número de errores. Estos logs no incluyen la entrada no válida.
## Elige qué registrar { #choose-what-to-record }
El diccionario `telemetry` también acepta estos ajustes:
| Ajuste | Propósito | Por defecto |
| --- | --- | --- |
| `tracing` | Registrar spans de requests HTTP y conexiones WebSocket | `True` |
| `metrics` | Registrar métricas de requests HTTP | `True` |
| `logs` | Registrar fallos de validación y excepciones no controladas | `True` |
| `operation_spans` | Añadir spans para las operaciones de las requests | `True` |
| `exclude` | Omitir requests cuando una función que recibe el scope de ASGI devuelve `True` | `None` |
| `auto_configure` | Añadir exportadores para los endpoints establecidos en variables de entorno | `True` |
Por ejemplo, para recopilar métricas excluyendo las verificaciones de estado:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Establece `auto_configure` en `False` cuando tu aplicación gestione por sí misma la configuración de los proveedores, por ejemplo, dentro de su función lifespan.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Lorsque votre API est en cours d'exécution, vous pouvez vouloir savoir quel trafic elle reçoit, quelles requêtes sont lentes et quand des erreurs se produisent.
La **télémétrie** désigne les données sur le comportement de votre application qui vous aident à répondre à ces questions. Les types courants comprennent :
- **Métriques** : des mesures que vous pouvez agréger au fil du temps, comme les temps de réponse et le nombre de requêtes en cours de traitement.
- **Traces** : des enregistrements de requêtes individuelles et des opérations effectuées pour les traiter. Chaque opération chronométrée est appelée un **span**.
- **Logs** : des enregistrements horodatés d'événements, comme le démarrage d'une application ou l'échec d'une opération.
[**OpenTelemetry**](https://opentelemetry.io/) est un ensemble de standards et d'outils permettant de collecter la télémétrie et de l'envoyer à un service de supervision, où vous pouvez l'explorer dans des tableaux de bord.
**FastAPI prend en charge OpenTelemetry par défaut** pour les traces, les métriques et les logs des requêtes HTTP. Les connexions WebSocket fournissent également des traces et des logs. Pour consulter ces données, configurez un service de supervision pour les recevoir.
## Installer FastAPI { #install-fastapi }
Installez FastAPI avec les dépendances optionnelles `standard`, qui incluent les paquets permettant d'envoyer la télémétrie :
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Créer l'application { #create-the-app }
Créez un fichier `main.py` :
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Remarquez que tout fonctionne par défaut : vous n'avez pas besoin d'écrire de code personnalisé pour que la télémétrie fonctionne.
## FastAPI Cloud { #fastapi-cloud }
Lorsque vous déployez sur [FastAPI Cloud](https://fastapicloud.com) avec `fastapi[standard]`, les métriques fonctionnent automatiquement. Vous n'avez rien d'autre à configurer.
Avec les offres Pro, vous pouvez consulter le nombre de requêtes, les taux d'erreur et les temps de réponse dans le [tableau de bord des métriques](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="Tableau de bord des métriques FastAPI Cloud Pro avec des exemples de données">
## Autres services de supervision { #other-monitoring-services }
Pour envoyer la télémétrie à un autre service de supervision, configurez un endpoint qui accepte **OTLP**, le protocole OpenTelemetry pour l'envoi de télémétrie. Utilisez l'endpoint de base HTTP/protobuf du service.
Définissez ces variables d'environnement en remplaçant l'URL d'exemple par votre endpoint :
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` identifie votre application dans le service de supervision. L'endpoint est l'URL de base pour recevoir les données. Les traces sont envoyées à `/v1/traces`, les métriques à `/v1/metrics` et les logs à `/v1/logs` sous cette URL.
Si votre service nécessite une authentification, définissez `OTEL_EXPORTER_OTLP_HEADERS` avec les en-têtes qu'il spécifie, par exemple `api-key=YOUR_API_KEY`.
## Exécuter l'application { #run-the-app }
Démarrez l'application dans le même terminal :
<div class="termy">
```console
$ uv run fastapi run
```
</div>
Dans un autre terminal, envoyez une requête :
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Ouvrez votre service de supervision et recherchez `my-api`. Après le prochain export, vous pouvez voir une trace avec un span `GET /items/{item_id}`, ainsi que des métriques sur le nombre de requêtes, la durée des réponses et les requêtes actives.
## Personnaliser la télémétrie { #customize-telemetry }
### Configurer les fournisseurs et les exportateurs { #configure-providers-and-exporters }
Un **fournisseur** fournit les objets qui enregistrent les traces, les métriques ou les logs. Sa configuration contrôle la façon dont ces données sont traitées et exportées.
Les bibliothèques de télémétrie peuvent configurer les fournisseurs globaux d'OpenTelemetry. Configurez la bibliothèque avant le démarrage de l'application, et FastAPI utilise automatiquement ces fournisseurs.
Lorsqu'un endpoint OTLP est défini dans l'environnement, FastAPI ajoute un exportateur pour cette destination à chaque fournisseur activé. Les exportateurs existants continuent d'envoyer des données vers leurs destinations.
Configurez chaque destination une seule fois. Si une autre bibliothèque gère déjà la destination définie dans l'environnement, désactivez son export vers cette destination ou désactivez la configuration automatique de FastAPI :
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Vous pouvez aussi passer un fournisseur directement dans le dictionnaire `telemetry`. Par exemple, ce fournisseur utilise l'exportateur console d'OpenTelemetry pour afficher les spans des requêtes dans votre terminal :
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
L'**exportateur** envoie les spans vers leur destination. `BatchSpanProcessor` regroupe les spans et les envoie en arrière-plan. Remplacez l'exportateur console par un exportateur fourni par votre bibliothèque de supervision pour utiliser sa destination. Consultez le [guide d'instrumentation Python d'OpenTelemetry](https://opentelemetry.io/docs/languages/python/instrumentation/) pour plus d'options de configuration.
Utilisez `meter_provider` ou `logger_provider` dans le même dictionnaire pour fournir un fournisseur de métriques ou de logs. L'application ou la bibliothèque qui crée un fournisseur gère son arrêt. FastAPI gère les composants d'export qu'il ajoute.
/// warning | Alertes
OpenTelemetry utilise des fournisseurs globaux par défaut. Une configuration indépendante de la télémétrie pour les [sous-applications montées](sub-applications.md) n'est pas garantie.
///
### Tracer les opérations des requêtes { #trace-request-operations }
Par défaut, les traces des requêtes incluent des spans pour la résolution des dépendances, l'exécution de votre fonction de chemin d'accès, la sérialisation de la réponse et l'exécution de chaque tâche dans `BackgroundTasks` de FastAPI. Ces spans utilisent le même fournisseur et les mêmes exportateurs.
Les spans des tâches en arrière-plan font toujours partie de la trace de la requête. Ils s'exécutent après la fin du span de la réponse HTTP et n'augmentent donc pas le temps de réponse mesuré.
Pour enregistrer uniquement le span de la requête HTTP, définissez `operation_spans` sur `False` :
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Tracer les connexions WebSocket { #trace-websocket-connections }
Chaque connexion WebSocket possède un span tel que `WS /ws/{room}`, qui couvre le gestionnaire et le nettoyage des dépendances. Il utilise les mêmes fournisseurs et paramètres, notamment `operation_spans` pour la résolution des dépendances et l'exécution de l'endpoint.
Les métriques des requêtes HTTP couvrent uniquement les requêtes HTTP. Les déconnexions WebSocket normales avec les codes `1000` ou `1001` ne produisent pas de logs d'erreur.
### Examiner les erreurs { #inspect-errors }
FastAPI enregistre les exceptions non gérées sous forme de logs OpenTelemetry, liés à la trace de la requête ou de la connexion. Les logs d'erreur sont enregistrés même lorsque la trace n'est pas échantillonnée.
Les logs d'exception incluent le type, le message et la trace de pile de l'exception. Les messages et les traces de pile peuvent contenir des informations sensibles. Utilisez les processeurs de logs de votre fournisseur pour les filtrer ou masquer ces informations, ou définissez `logs` sur `False` pour désactiver ces logs.
FastAPI enregistre également les échecs de validation des requêtes sous forme de logs d'avertissement avec la route et le nombre d'erreurs. Ces logs n'incluent pas les données d'entrée invalides.
## Choisir les données à enregistrer { #choose-what-to-record }
Le dictionnaire `telemetry` accepte également ces paramètres :
| Paramètre | Fonction | Valeur par défaut |
| --- | --- | --- |
| `tracing` | Enregistrer les spans des requêtes HTTP et des connexions WebSocket | `True` |
| `metrics` | Enregistrer les métriques des requêtes HTTP | `True` |
| `logs` | Enregistrer les échecs de validation et les exceptions non gérées | `True` |
| `operation_spans` | Ajouter des spans pour les opérations des requêtes | `True` |
| `exclude` | Ignorer les requêtes lorsqu'une fonction recevant le scope ASGI renvoie `True` | `None` |
| `auto_configure` | Ajouter des exportateurs pour les endpoints définis dans les variables d'environnement | `True` |
Par exemple, pour collecter des métriques tout en excluant les vérifications de l'état de santé :
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Définissez `auto_configure` sur `False` lorsque votre application gère elle-même la configuration des fournisseurs, par exemple dans sa fonction lifespan.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
जब आपका API चल रहा होता है, तो आप शायद जानना चाहें कि उसे कितना ट्रैफ़िक मिल रहा है, कौन-से requests धीमे हैं और त्रुटियाँ कब होती हैं।
**Telemetry** आपके एप्लिकेशन के व्यवहार से जुड़ा data है, जो इन सवालों के जवाब देने में मदद करता है। इसके सामान्य प्रकार हैं:
- **Metrics**: ऐसे माप जिन्हें आप समय के साथ संक्षेप में देख सकते हैं, जैसे response में लगने वाला समय और संभाले जा रहे requests की संख्या।
- **Traces**: अलग-अलग requests और उन्हें संभालने के लिए किए गए ऑपरेशन के रिकॉर्ड। हर ऑपरेशन जिसका समय मापा जाता है, एक **span** कहलाता है।
- **Logs**: टाइमस्टैम्प के साथ event के रिकॉर्ड, जैसे किसी एप्लिकेशन का शुरू होना या किसी ऑपरेशन का विफल होना।
[**OpenTelemetry**](https://opentelemetry.io/) telemetry एकत्र करने और उसे किसी मॉनिटरिंग सेवा को भेजने के लिए standards और tools का एक समूह है, जहाँ आप उसे डैशबोर्ड में देख और जाँच सकते हैं।
**FastAPI, HTTP request traces, metrics और logs के लिए default रूप से OpenTelemetry support प्रदान करता है**। WebSocket कनेक्शन भी traces और logs प्रदान करते हैं। उस data को देखने के लिए, उसे प्राप्त करने वाली मॉनिटरिंग सेवा कॉन्फ़िगर करें।
## FastAPI install करें { #install-fastapi }
FastAPI को `standard` extras के साथ install करें, जिनमें telemetry भेजने के लिए packages शामिल हैं:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## ऐप बनाएँ { #create-the-app }
एक file `main.py` बनाएँ:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
ध्यान दें कि यह सब default रूप से काम करता है। Telemetry के काम करने के लिए आपको कोई कस्टम कोड लिखने की ज़रूरत नहीं है।
## FastAPI Cloud { #fastapi-cloud }
जब आप `fastapi[standard]` के साथ [FastAPI Cloud](https://fastapicloud.com) पर डिप्लॉय करते हैं, तो metrics अपने आप काम करते हैं। आपको कुछ और कॉन्फ़िगर नहीं करना पड़ता।
Pro प्लान पर, आप [Metrics डैशबोर्ड](https://fastapicloud.com/docs/monitoring-and-performance/metrics/) में requests की संख्या, त्रुटि दर और response में लगने वाला समय देख सकते हैं।
<img src="/img/tutorial/opentelemetry/image01.png" alt="उदाहरण data के साथ FastAPI Cloud Pro metrics डैशबोर्ड">
## अन्य मॉनिटरिंग सेवाएँ { #other-monitoring-services }
किसी अन्य मॉनिटरिंग सेवा को telemetry भेजने के लिए, ऐसा endpoint कॉन्फ़िगर करें जो **OTLP** स्वीकार करता हो। यह telemetry भेजने के लिए OpenTelemetry का प्रोटोकॉल है। सेवा के HTTP/protobuf बेस endpoint का उपयोग करें।
उदाहरण URL की जगह अपना endpoint डालकर, ये environment variables सेट करें:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` मॉनिटरिंग सेवा में आपके ऐप की पहचान करता है। Endpoint, data प्राप्त करने के लिए बेस URL है। उस URL के अंतर्गत traces को `/v1/traces`, metrics को `/v1/metrics` और logs को `/v1/logs` पर भेजा जाता है।
यदि आपकी सेवा को प्रमाणीकरण की ज़रूरत है, तो `OTEL_EXPORTER_OTLP_HEADERS` में उसके बताए गए headers सेट करें, उदाहरण के लिए `api-key=YOUR_API_KEY`।
## ऐप चलाएँ { #run-the-app }
उसी टर्मिनल में ऐप शुरू करें:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
दूसरे टर्मिनल में, एक request भेजें:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
अपनी मॉनिटरिंग सेवा खोलें और `my-api` खोजें। अगले एक्सपोर्ट के बाद, आप `GET /items/{item_id}` span वाला एक trace देख सकते हैं, साथ ही requests की संख्या, response की अवधि और सक्रिय requests के metrics भी देख सकते हैं।
## Telemetry को अपनी ज़रूरत के अनुसार बदलें { #customize-telemetry }
### Providers और exporters कॉन्फ़िगर करें { #configure-providers-and-exporters }
एक **provider** ऐसे ऑब्जेक्ट प्रदान करता है जो traces, metrics या logs रिकॉर्ड करते हैं। उसका कॉन्फ़िगरेशन नियंत्रित करता है कि उस data को कैसे प्रोसेस और एक्सपोर्ट किया जाता है।
Telemetry लाइब्रेरी OpenTelemetry के ग्लोबल providers को कॉन्फ़िगर कर सकती हैं। ऐप शुरू होने से पहले लाइब्रेरी को कॉन्फ़िगर करें, और FastAPI अपने आप उन providers का उपयोग करता है।
जब environment में कोई OTLP endpoint सेट होता है, तो FastAPI हर सक्षम provider में उस गंतव्य के लिए एक exporter जोड़ता है। मौजूदा exporters अपने गंतव्यों पर data भेजना जारी रखते हैं।
हर गंतव्य को एक बार कॉन्फ़िगर करें। यदि कोई दूसरी लाइब्रेरी पहले से ही environment में दिए गए गंतव्य को संभाल रही है, तो उसका environment export अक्षम करें या FastAPI का स्वचालित setup बंद करें:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
आप `telemetry` डिक्शनरी में सीधे एक provider भी पास कर सकते हैं। उदाहरण के लिए, यह provider आपके टर्मिनल में request spans प्रिंट करने के लिए OpenTelemetry के console exporter का उपयोग करता है:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**Exporter**, spans को उनके गंतव्य पर भेजता है। `BatchSpanProcessor`, spans को समूहों में बाँटता है और उन्हें बैकग्राउंड में भेजता है। अपनी मॉनिटरिंग लाइब्रेरी के गंतव्य का उपयोग करने के लिए console exporter की जगह उस लाइब्रेरी द्वारा प्रदान किया गया exporter लगाएँ। कॉन्फ़िगरेशन के और विकल्पों के लिए [OpenTelemetry की Python instrumentation गाइड](https://opentelemetry.io/docs/languages/python/instrumentation/) देखें।
Metrics या logs provider प्रदान करने के लिए उसी डिक्शनरी में `meter_provider` या `logger_provider` का उपयोग करें। Provider बनाने वाला एप्लिकेशन या लाइब्रेरी उसके shutdown को संभालता है। FastAPI अपने जोड़े गए एक्सपोर्ट घटकों को संभालता है।
/// warning | चेतावनी
OpenTelemetry default रूप से ग्लोबल providers का उपयोग करता है। [माउंट किए गए उप-एप्लिकेशन](sub-applications.md) के लिए स्वतंत्र telemetry कॉन्फ़िगरेशन की गारंटी नहीं है।
///
### Request ऑपरेशन को ट्रेस करें { #trace-request-operations }
Default रूप से, request traces में dependencies रिज़ॉल्व करने, आपका path operation function चलाने, response को serialize करने और FastAPI के `BackgroundTasks` में हर टास्क को चलाने के spans शामिल होते हैं। ये spans उसी provider और उन्हीं exporters का उपयोग करते हैं।
बैकग्राउंड टास्क के spans, request के trace का हिस्सा बने रहते हैं। वे HTTP response span समाप्त होने के बाद चलते हैं, इसलिए वे मापे गए response समय को नहीं बढ़ाते।
केवल HTTP request span रिकॉर्ड करने के लिए, `operation_spans` को `False` पर सेट करें:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### WebSocket कनेक्शन को ट्रेस करें { #trace-websocket-connections }
हर WebSocket कनेक्शन का एक span होता है, जैसे `WS /ws/{room}`, जो हैंडलर और dependency cleanup को कवर करता है। यह उन्हीं providers और सेटिंग्स का उपयोग करता है, जिनमें dependency resolution और endpoint के निष्पादन के लिए `operation_spans` भी शामिल है।
HTTP request metrics में केवल HTTP requests शामिल होते हैं। कोड `1000` या `1001` के साथ होने वाले सामान्य WebSocket डिस्कनेक्ट, error logs नहीं बनाते।
### त्रुटियों की जाँच करें { #inspect-errors }
FastAPI, संभाले न गए exceptions को OpenTelemetry logs के रूप में रिकॉर्ड करता है, जो request या कनेक्शन के trace से जुड़े होते हैं। Error logs तब भी रिकॉर्ड किए जाते हैं जब trace को सैंपल नहीं किया जाता।
Exception logs में exception का प्रकार, संदेश और stack trace शामिल होते हैं। संदेशों और stack traces में संवेदनशील जानकारी हो सकती है। उन्हें फ़िल्टर करने या उनमें से संवेदनशील जानकारी हटाने के लिए अपने provider के log processors का उपयोग करें, या इन logs को अक्षम करने के लिए `logs` को `False` पर सेट करें।
FastAPI, request validation की विफलताओं को भी route और त्रुटियों की संख्या के साथ warning logs के रूप में रिकॉर्ड करता है। इन logs में अमान्य इनपुट शामिल नहीं होता।
## चुनें कि क्या रिकॉर्ड करना है { #choose-what-to-record }
`telemetry` डिक्शनरी इन सेटिंग्स को भी स्वीकार करती है:
| सेटिंग | उद्देश्य | Default |
| --- | --- | --- |
| `tracing` | HTTP request और WebSocket कनेक्शन के spans रिकॉर्ड करना | `True` |
| `metrics` | HTTP request metrics रिकॉर्ड करना | `True` |
| `logs` | Validation की विफलताएँ और संभाले न गए exceptions रिकॉर्ड करना | `True` |
| `operation_spans` | Request ऑपरेशन के लिए spans जोड़ना | `True` |
| `exclude` | जब ASGI scope प्राप्त करने वाला function `True` लौटाए, तो requests छोड़ देना | `None` |
| `auto_configure` | Environment variables में सेट endpoints के लिए exporters जोड़ना | `True` |
उदाहरण के लिए, health checks को छोड़कर metrics एकत्र करने के लिए:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
जब आपका एप्लिकेशन provider setup खुद संभालता हो, जैसे अपने lifespan function के अंदर, तो `auto_configure` को `False` पर सेट करें।
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
APIを実行しているとき、受信するトラフィックの量、遅いリクエスト、エラーが発生するタイミングを把握したくなることがあります。
**テレメトリー**は、こうした疑問に答えるためのアプリケーションの動作に関するデータです。一般的には、次の種類があります:
- **メトリクス**: レスポンス時間や処理中のリクエスト数など、時間の経過に沿って集計できる測定値です。
- **トレース**: 個々のリクエストと、それを処理するために実行された操作の記録です。時間を計測する各操作を**スパン**と呼びます。
- **ログ**: アプリケーションの起動や操作の失敗など、イベントのタイムスタンプ付きの記録です。
[**OpenTelemetry**](https://opentelemetry.io/)は、テレメトリーを収集して監視サービスに送信するための標準とツールのセットです。監視サービスのダッシュボードで、そのデータを確認できます。
**FastAPIはデフォルトでOpenTelemetryをサポートしており**、HTTPリクエストのトレース、メトリクス、ログを提供します。WebSocket接続でもトレースとログを提供します。このデータを確認するには、受信する監視サービスを設定してください。
## FastAPIのインストール { #install-fastapi }
テレメトリーを送信するためのパッケージを含む`standard`オプションを指定してFastAPIをインストールします:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## アプリの作成 { #create-the-app }
`main.py`ファイルを作成します:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
すべてデフォルトで動作するため、テレメトリーを有効にするための独自のコードを書く必要はありません。
## FastAPI Cloud { #fastapi-cloud }
`fastapi[standard]`を使用して[FastAPI Cloud](https://fastapicloud.com)にデプロイすると、メトリクスは自動的に機能します。追加の設定は不要です。
Proプランでは、[メトリクスダッシュボード](https://fastapicloud.com/docs/monitoring-and-performance/metrics/)でリクエスト数、エラー率、レスポンス時間を確認できます。
<img src="/img/tutorial/opentelemetry/image01.png" alt="サンプルデータを表示したFastAPI Cloud Proのメトリクスダッシュボード">
## その他の監視サービス { #other-monitoring-services }
別の監視サービスにテレメトリーを送信するには、テレメトリー送信用のOpenTelemetryプロトコルである**OTLP**を受け付けるエンドポイントを設定します。サービスのHTTP/protobufベースエンドポイントを使用してください。
例のURLを自分のエンドポイントに置き換えて、次の環境変数を設定します:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME`は、監視サービス内でアプリを識別するための名前です。エンドポイントはデータを受信するためのベースURLです。そのURL配下の`/v1/traces`にトレース、`/v1/metrics`にメトリクス、`/v1/logs`にログが送信されます。
サービスで認証が必要な場合は、`OTEL_EXPORTER_OTLP_HEADERS`にサービスが指定するヘッダー(例: `api-key=YOUR_API_KEY`)を設定してください。
## アプリの実行 { #run-the-app }
同じターミナルでアプリを起動します:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
別のターミナルでリクエストを送信します:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
監視サービスを開き、`my-api`を探します。次のエクスポート後に、`GET /items/{item_id}`スパンを含むトレースと、リクエスト数、レスポンス時間、処理中のリクエスト数のメトリクスを確認できます。
## テレメトリーのカスタマイズ { #customize-telemetry }
### プロバイダーとエクスポーターの設定 { #configure-providers-and-exporters }
**プロバイダー**は、トレース、メトリクス、ログを記録するオブジェクトを提供します。その設定によって、データの処理方法とエクスポート方法を制御します。
テレメトリーライブラリは、OpenTelemetryのグローバルプロバイダーを設定できます。アプリの起動前にライブラリを設定すると、FastAPIはそれらのプロバイダーを自動的に使用します。
環境変数にOTLPエンドポイントが設定されている場合、FastAPIは有効な各プロバイダーに、その送信先のエクスポーターを追加します。既存のエクスポーターは、それぞれの送信先にデータを送信し続けます。
各送信先は一度だけ設定してください。別のライブラリがすでに環境変数で指定された送信先への送信を処理している場合は、そのライブラリの環境変数に基づくエクスポートを無効にするか、FastAPIの自動設定を無効にします:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
`telemetry`辞書にプロバイダーを直接渡すこともできます。例えば、次のプロバイダーはOpenTelemetryのコンソールエクスポーターを使用して、リクエストのスパンをターミナルに出力します:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**エクスポーター**は、スパンを送信先に送ります。`BatchSpanProcessor`はスパンをまとめ、バックグラウンドで送信します。監視ライブラリの送信先を使用するには、コンソールエクスポーターをそのライブラリが提供するエクスポーターに置き換えてください。その他の設定オプションについては、[OpenTelemetryのPython計装ガイド](https://opentelemetry.io/docs/languages/python/instrumentation/)を参照してください。
メトリクスやログのプロバイダーを指定するには、同じ辞書内で`meter_provider`または`logger_provider`を使用します。プロバイダーを作成したアプリケーションまたはライブラリが、そのシャットダウンを管理します。FastAPIは、自身が追加したエクスポートコンポーネントを管理します。
/// warning | 注意
OpenTelemetryはデフォルトでグローバルプロバイダーを使用します。[マウントされたサブアプリケーション](sub-applications.md)ごとに独立したテレメトリー設定は保証されません。
///
### リクエスト操作のトレース { #trace-request-operations }
デフォルトでは、リクエストのトレースには、依存関係の解決、path operation関数の実行、レスポンスのシリアライズ、FastAPIの`BackgroundTasks`内の各タスクの実行に対応するスパンが含まれます。これらのスパンは同じプロバイダーとエクスポーターを使用します。
バックグラウンドタスクのスパンも、リクエストのトレースの一部として扱われます。これらはHTTPレスポンスのスパンが終了した後に実行されるため、測定されるレスポンス時間は増加しません。
HTTPリクエストのスパンのみを記録するには、`operation_spans`を`False`に設定します:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### WebSocket接続のトレース { #trace-websocket-connections }
各WebSocket接続には、`WS /ws/{room}`のようなスパンがあり、ハンドラーと依存関係のクリーンアップを対象とします。同じプロバイダーと設定を使用し、依存関係の解決とエンドポイントの実行には`operation_spans`の設定も適用されます。
HTTPリクエストのメトリクスは、HTTPリクエストのみを対象とします。コード`1000`または`1001`による正常なWebSocket切断では、エラーログは生成されません。
### エラーの確認 { #inspect-errors }
FastAPIは、未処理の例外をOpenTelemetryのログとして記録し、リクエストまたは接続のトレースに関連付けます。トレースがサンプリングされていない場合でも、エラーログは記録されます。
例外ログには、例外の型、メッセージ、スタックトレースが含まれます。メッセージやスタックトレースには機密情報が含まれる可能性があります。プロバイダーのログプロセッサーを使用してフィルタリングやマスキングを行うか、`logs`を`False`に設定してこれらのログを無効にしてください。
FastAPIは、リクエストのバリデーション失敗も、ルートとエラー数を含む警告ログとして記録します。これらのログに無効な入力値は含まれません。
## 記録対象の選択 { #choose-what-to-record }
`telemetry`辞書では、次の設定も指定できます:
| 設定 | 目的 | デフォルト |
| --- | --- | --- |
| `tracing` | HTTPリクエストとWebSocket接続のスパンを記録します | `True` |
| `metrics` | HTTPリクエストのメトリクスを記録します | `True` |
| `logs` | バリデーション失敗と未処理の例外を記録します | `True` |
| `operation_spans` | リクエスト操作のスパンを追加します | `True` |
| `exclude` | ASGIスコープを受け取る関数が`True`を返した場合、そのリクエストを除外します | `None` |
| `auto_configure` | 環境変数に設定されたエンドポイントのエクスポーターを追加します | `True` |
例えば、ヘルスチェックを除外してメトリクスを収集するには、次のようにします:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
lifespan関数内などで、アプリケーション自身がプロバイダーの設定を行う場合は、`auto_configure`を`False`に設定してください。
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
API가 실행 중일 때 얼마나 많은 트래픽을 받는지, 어떤 요청이 느린지, 언제 오류가 발생하는지 알고 싶을 수 있습니다.
**텔레메트리**는 이러한 질문에 답하는 데 도움이 되는 애플리케이션 동작에 관한 데이터입니다. 일반적인 유형은 다음과 같습니다:
- **메트릭**: 응답 시간이나 처리 중인 요청 수처럼 시간에 따라 요약할 수 있는 측정값입니다.
- **트레이스**: 개별 요청과 해당 요청을 처리하기 위해 수행한 작업의 기록입니다. 소요 시간을 측정하는 각 작업을 **스팬**이라고 합니다.
- **로그**: 애플리케이션 시작이나 작업 실패와 같은 이벤트에 타임스탬프를 붙인 기록입니다.
[**OpenTelemetry**](https://opentelemetry.io/)는 텔레메트리를 수집하여 모니터링 서비스로 보내기 위한 표준과 도구 모음입니다. 모니터링 서비스에서는 대시보드를 통해 데이터를 살펴볼 수 있습니다.
**FastAPI는 기본적으로 OpenTelemetry를 지원**하며, HTTP 요청의 트레이스, 메트릭, 로그를 제공합니다. WebSocket 연결도 트레이스와 로그를 제공합니다. 이 데이터를 보려면 모니터링 서비스에서 데이터를 수신하도록 설정하세요.
## FastAPI 설치하기 { #install-fastapi }
텔레메트리 전송용 패키지가 포함된 `standard` 추가 옵션으로 FastAPI를 설치하세요:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## 애플리케이션 만들기 { #create-the-app }
`main.py` 파일을 만드세요:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
이 모든 기능은 기본적으로 작동하며, 텔레메트리를 사용하기 위해 별도의 코드를 작성할 필요가 없습니다.
## FastAPI Cloud { #fastapi-cloud }
`fastapi[standard]`를 사용하여 [FastAPI Cloud](https://fastapicloud.com)에 배포하면 메트릭이 자동으로 작동합니다. 다른 설정은 필요하지 않습니다.
Pro 요금제에서는 [메트릭 대시보드](https://fastapicloud.com/docs/monitoring-and-performance/metrics/)에서 요청 수, 오류율, 응답 시간을 확인할 수 있습니다.
<img src="/img/tutorial/opentelemetry/image01.png" alt="예제 데이터가 표시된 FastAPI Cloud Pro 메트릭 대시보드">
## 다른 모니터링 서비스 { #other-monitoring-services }
다른 모니터링 서비스로 텔레메트리를 보내려면 텔레메트리 전송을 위한 OpenTelemetry 프로토콜인 **OTLP**를 지원하는 엔드포인트를 설정하세요. 해당 서비스의 HTTP/protobuf 기본 엔드포인트를 사용하세요.
예제 URL을 사용할 엔드포인트로 바꾸고 다음 환경 변수를 설정하세요:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME`은 모니터링 서비스에서 애플리케이션을 식별합니다. 엔드포인트는 데이터를 수신하는 기본 URL입니다. 트레이스는 해당 URL 아래의 `/v1/traces`로, 메트릭은 `/v1/metrics`로, 로그는 `/v1/logs`로 전송됩니다.
서비스에서 인증을 요구하는 경우, `OTEL_EXPORTER_OTLP_HEADERS`를 해당 서비스에서 지정한 헤더로 설정하세요. 예를 들면 `api-key=YOUR_API_KEY`입니다.
## 애플리케이션 실행하기 { #run-the-app }
같은 터미널에서 애플리케이션을 시작하세요:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
다른 터미널에서 요청을 보내세요:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
모니터링 서비스를 열고 `my-api`를 찾으세요. 다음 내보내기가 이루어진 후, `GET /items/{item_id}` 스팬이 포함된 트레이스와 함께 요청 수, 응답 소요 시간, 활성 요청에 대한 메트릭을 확인할 수 있습니다.
## 텔레메트리 사용자 정의하기 { #customize-telemetry }
### 프로바이더와 익스포터 설정하기 { #configure-providers-and-exporters }
**프로바이더**는 트레이스, 메트릭 또는 로그를 기록하는 객체를 제공합니다. 프로바이더 설정은 해당 데이터의 처리 및 내보내기 방식을 제어합니다.
텔레메트리 라이브러리는 OpenTelemetry의 전역 프로바이더를 설정할 수 있습니다. 애플리케이션이 시작되기 전에 라이브러리를 설정하면 FastAPI가 해당 프로바이더를 자동으로 사용합니다.
환경 변수에 OTLP 엔드포인트가 설정되어 있으면, FastAPI는 활성화된 각 프로바이더에 해당 대상으로 데이터를 보내는 익스포터를 추가합니다. 기존 익스포터는 계속해서 각자의 대상으로 데이터를 보냅니다.
각 대상은 한 번만 설정하세요. 다른 라이브러리가 이미 환경 변수에 지정된 대상으로의 전송을 처리하고 있다면, 해당 라이브러리의 환경 변수 기반 내보내기를 비활성화하거나 FastAPI의 자동 설정을 끄세요:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
`telemetry` 딕셔너리에 프로바이더를 직접 전달할 수도 있습니다. 예를 들어, 다음 프로바이더는 OpenTelemetry의 콘솔 익스포터를 사용하여 터미널에 요청 스팬을 출력합니다:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**익스포터**는 스팬을 대상으로 보냅니다. `BatchSpanProcessor`는 스팬을 그룹으로 묶어 백그라운드에서 보냅니다. 모니터링 라이브러리의 대상으로 보내려면 콘솔 익스포터를 해당 라이브러리에서 제공하는 익스포터로 바꾸세요. 더 많은 설정 옵션은 [OpenTelemetry의 Python 계측 가이드](https://opentelemetry.io/docs/languages/python/instrumentation/)를 참고하세요.
같은 딕셔너리에서 `meter_provider` 또는 `logger_provider`를 사용하여 메트릭 또는 로그 프로바이더를 제공하세요. 프로바이더를 생성한 애플리케이션이나 라이브러리가 해당 프로바이더의 종료를 관리합니다. FastAPI는 자신이 추가한 내보내기 구성 요소를 관리합니다.
/// warning | 경고
OpenTelemetry는 기본적으로 전역 프로바이더를 사용합니다. [마운트된 하위 애플리케이션](sub-applications.md)에 대한 독립적인 텔레메트리 설정은 보장되지 않습니다.
///
### 요청 작업 추적하기 { #trace-request-operations }
기본적으로 요청 트레이스에는 의존성 해결, 경로 처리 함수 실행, 응답 직렬화, FastAPI의 `BackgroundTasks`에 있는 각 작업 실행에 대한 스팬이 포함됩니다. 이 스팬들은 동일한 프로바이더와 익스포터를 사용합니다.
백그라운드 작업의 스팬도 해당 요청의 트레이스에 포함됩니다. 백그라운드 작업은 HTTP 응답 스팬이 끝난 후에 실행되므로 측정된 응답 시간을 늘리지 않습니다.
HTTP 요청 스팬만 기록하려면 `operation_spans`를 `False`로 설정하세요:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### WebSocket 연결 추적하기 { #trace-websocket-connections }
각 WebSocket 연결에는 `WS /ws/{room}`과 같은 스팬이 있으며, 핸들러 실행과 의존성 정리 작업을 포함합니다. 이 스팬은 동일한 프로바이더와 설정을 사용하며, 여기에는 의존성 해결과 엔드포인트 실행을 위한 `operation_spans`도 포함됩니다.
HTTP 요청 메트릭은 HTTP 요청만 다룹니다. 코드 `1000` 또는 `1001`로 정상 종료된 WebSocket 연결은 오류 로그를 생성하지 않습니다.
### 오류 확인하기 { #inspect-errors }
FastAPI는 처리되지 않은 예외를 OpenTelemetry 로그로 기록하고, 해당 요청이나 연결의 트레이스에 연결합니다. 트레이스가 샘플링되지 않은 경우에도 오류 로그는 기록됩니다.
예외 로그에는 예외 유형, 메시지, 스택 트레이스가 포함됩니다. 메시지와 스택 트레이스에는 민감한 정보가 포함될 수 있습니다. 프로바이더의 로그 프로세서를 사용하여 이를 필터링하거나 민감한 정보를 가리세요. 또는 `logs`를 `False`로 설정하여 이 로그를 비활성화하세요.
FastAPI는 요청 유효성 검사 실패도 경로와 오류 수가 포함된 경고 로그로 기록합니다. 이 로그에는 유효하지 않은 입력값이 포함되지 않습니다.
## 기록할 항목 선택하기 { #choose-what-to-record }
`telemetry` 딕셔너리는 다음 설정도 지원합니다:
| 설정 | 용도 | 기본값 |
| --- | --- | --- |
| `tracing` | HTTP 요청 및 WebSocket 연결 스팬 기록 | `True` |
| `metrics` | HTTP 요청 메트릭 기록 | `True` |
| `logs` | 유효성 검사 실패 및 처리되지 않은 예외 기록 | `True` |
| `operation_spans` | 요청 작업에 대한 스팬 추가 | `True` |
| `exclude` | ASGI 스코프를 전달받는 함수가 `True`를 반환하면 해당 요청 제외 | `None` |
| `auto_configure` | 환경 변수에 설정된 엔드포인트에 대한 익스포터 추가 | `True` |
예를 들어, 상태 확인 요청을 제외하고 메트릭을 수집하려면 다음과 같이 설정하세요:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
애플리케이션이 lifespan 함수 내부 등에서 프로바이더 설정을 직접 처리하는 경우 `auto_configure`를 `False`로 설정하세요.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Quando sua API está em execução, você pode querer saber quanto tráfego ela recebe, quais requests são lentos e quando ocorrem erros.
**Telemetria** são dados sobre o comportamento da sua aplicação que ajudam a responder a essas perguntas. Os tipos comuns incluem:
- **Métricas**: medições que você pode resumir ao longo do tempo, como os tempos de response e o número de requests em processamento.
- **Traces**: registros de requests individuais e das operações realizadas para processá-los. Cada operação cronometrada é chamada de **span**.
- **Logs**: registros de eventos com marcação de tempo, como a inicialização de uma aplicação ou a falha de uma operação.
[**OpenTelemetry**](https://opentelemetry.io/) é um conjunto de padrões e ferramentas para coletar telemetria e enviá-la a um serviço de monitoramento, onde você pode explorá-la em painéis.
**O FastAPI oferece suporte a OpenTelemetry por padrão** para traces de requests HTTP, métricas e logs. As conexões WebSocket também fornecem traces e logs. Para visualizar esses dados, configure um serviço de monitoramento para recebê-los.
## Instale o FastAPI { #install-fastapi }
Instale o FastAPI com os extras `standard`, que incluem os pacotes para enviar telemetria:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Crie a aplicação { #create-the-app }
Crie um arquivo `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Observe que tudo funciona por padrão, sem precisar escrever código personalizado para a telemetria funcionar.
## FastAPI Cloud { #fastapi-cloud }
Ao fazer o deploy no [FastAPI Cloud](https://fastapicloud.com) com `fastapi[standard]`, as métricas funcionam automaticamente. Não é necessário configurar mais nada.
Nos planos Pro, você pode visualizar contagens de requests, taxas de erro e tempos de response no [painel de métricas](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="Painel de métricas do FastAPI Cloud Pro com dados de exemplo">
## Outros serviços de monitoramento { #other-monitoring-services }
Para enviar telemetria a outro serviço de monitoramento, configure um endpoint que aceite **OTLP**, o protocolo do OpenTelemetry para enviar telemetria. Use o endpoint base HTTP/protobuf do serviço.
Defina estas variáveis de ambiente, substituindo a URL de exemplo pelo seu endpoint:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` identifica sua aplicação no serviço de monitoramento. O endpoint é a URL base para receber dados. Os traces são enviados para `/v1/traces`, as métricas para `/v1/metrics` e os logs para `/v1/logs` nessa URL.
Se o serviço exigir autenticação, defina `OTEL_EXPORTER_OTLP_HEADERS` com os cabeçalhos especificados por ele, por exemplo, `api-key=YOUR_API_KEY`.
## Execute a aplicação { #run-the-app }
Inicie a aplicação no mesmo terminal:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
Em outro terminal, envie um request:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Abra seu serviço de monitoramento e procure `my-api`. Após a próxima exportação, você poderá ver um trace com um span `GET /items/{item_id}`, junto com métricas de contagem de requests, duração de responses e requests ativos.
## Personalize a telemetria { #customize-telemetry }
### Configure provedores e exportadores { #configure-providers-and-exporters }
Um **provedor** fornece os objetos que registram traces, métricas ou logs. Sua configuração controla como esses dados são processados e exportados.
Bibliotecas de telemetria podem configurar os provedores globais do OpenTelemetry. Configure a biblioteca antes de iniciar a aplicação, e o FastAPI usará esses provedores automaticamente.
Quando um endpoint OTLP é definido no ambiente, o FastAPI adiciona um exportador para esse destino a cada provedor habilitado. Os exportadores existentes continuam enviando dados aos seus destinos.
Configure cada destino uma única vez. Se outra biblioteca já cuida do destino definido no ambiente, desabilite a exportação dela para esse destino ou desative a configuração automática do FastAPI:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Você também pode passar um provedor diretamente no dicionário `telemetry`. Por exemplo, este provedor usa o exportador de console do OpenTelemetry para exibir spans de requests no seu terminal:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
O **exportador** envia os spans ao destino. `BatchSpanProcessor` agrupa spans e os envia em segundo plano. Substitua o exportador de console por um fornecido pela sua biblioteca de monitoramento para usar o destino dela. Consulte o [tutorial de instrumentação Python do OpenTelemetry](https://opentelemetry.io/docs/languages/python/instrumentation/) para mais opções de configuração.
Use `meter_provider` ou `logger_provider` no mesmo dicionário para fornecer um provedor de métricas ou logs. A aplicação ou biblioteca que cria um provedor gerencia seu encerramento. O FastAPI gerencia os componentes de exportação que adiciona.
/// warning | Atenção
O OpenTelemetry usa provedores globais por padrão. A configuração independente de telemetria para [subaplicações montadas](sub-applications.md) não é garantida.
///
### Rastreie operações de requests { #trace-request-operations }
Por padrão, os traces de requests incluem spans para resolver dependências, executar sua função de operação de rota, serializar a response e executar cada tarefa em `BackgroundTasks` do FastAPI. Esses spans usam o mesmo provedor e os mesmos exportadores.
Os spans de tarefas em segundo plano continuam fazendo parte do trace do request. Eles são executados após o término do span da response HTTP, portanto não aumentam o tempo de response medido.
Para registrar apenas o span do request HTTP, defina `operation_spans` como `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Rastreie conexões WebSocket { #trace-websocket-connections }
Cada conexão WebSocket tem um span como `WS /ws/{room}`, que abrange o manipulador e a limpeza das dependências. Ele usa os mesmos provedores e configurações, incluindo `operation_spans` para a resolução de dependências e a execução do endpoint.
As métricas de requests HTTP abrangem apenas requests HTTP. Desconexões normais de WebSocket com os códigos `1000` ou `1001` não produzem logs de erro.
### Inspecione erros { #inspect-errors }
O FastAPI registra exceções não tratadas como logs do OpenTelemetry, vinculados ao trace do request ou da conexão. Os logs de erro são registrados mesmo quando o trace não é selecionado na amostragem.
Os logs de exceções incluem o tipo, a mensagem e o stack trace da exceção. Mensagens e stack traces podem conter informações sensíveis. Use os processadores de logs do seu provedor para filtrar ou ocultar essas informações, ou defina `logs` como `False` para desabilitar esses logs.
O FastAPI também registra falhas de validação de requests como logs de aviso com a rota e a contagem de erros. Esses logs não incluem os dados de entrada inválidos.
## Escolha o que registrar { #choose-what-to-record }
O dicionário `telemetry` também aceita estas configurações:
| Configuração | Finalidade | Padrão |
| --- | --- | --- |
| `tracing` | Registrar spans de requests HTTP e conexões WebSocket | `True` |
| `metrics` | Registrar métricas de requests HTTP | `True` |
| `logs` | Registrar falhas de validação e exceções não tratadas | `True` |
| `operation_spans` | Adicionar spans para operações de requests | `True` |
| `exclude` | Ignorar requests quando uma função que recebe o escopo ASGI retorna `True` | `None` |
| `auto_configure` | Adicionar exportadores para endpoints definidos em variáveis de ambiente | `True` |
Por exemplo, para coletar métricas excluindo verificações de integridade:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Defina `auto_configure` como `False` quando sua aplicação cuidar da configuração dos provedores por conta própria, por exemplo, dentro da função lifespan.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Когда ваш API запущен, вам может понадобиться узнать, сколько трафика он получает, какие HTTP-запросы выполняются медленно и когда происходят ошибки.
**Телеметрия** — это данные о поведении вашего приложения, которые помогают ответить на эти вопросы. Распространённые типы включают:
- **Метрики**: измерения, которые можно агрегировать за период времени, например время ответа и количество обрабатываемых HTTP-запросов.
- **Трейсы**: записи отдельных HTTP-запросов и операций, выполненных для их обработки. Каждая операция с измеренным временем называется **спан**.
- **Логи**: записи событий с временными метками, например запуск приложения или сбой операции.
[**OpenTelemetry**](https://opentelemetry.io/) — это набор стандартов и инструментов для сбора телеметрии и отправки её в сервис мониторинга, где её можно просматривать на дашбордах.
**FastAPI по умолчанию предоставляет поддержку OpenTelemetry** для трейсов HTTP-запросов, метрик и логов. WebSocket-соединения также предоставляют трейсы и логи. Чтобы увидеть эти данные, настройте сервис мониторинга для их получения.
## Установите FastAPI { #install-fastapi }
Установите FastAPI с дополнительными зависимостями `standard`, которые включают пакеты для отправки телеметрии:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Создайте приложение { #create-the-app }
Создайте файл `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Обратите внимание, что всё это работает по умолчанию, вам не нужно писать какой-либо пользовательский код, чтобы телеметрия работала.
## FastAPI Cloud { #fastapi-cloud }
Когда вы развёртываете приложение в [FastAPI Cloud](https://fastapicloud.com) с `fastapi[standard]`, метрики работают автоматически. Вам не нужно настраивать что-либо ещё.
На тарифах Pro вы можете просматривать количество HTTP-запросов, долю ошибок и время ответа в [дашборде метрик](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="Дашборд метрик FastAPI Cloud Pro с примером данных">
## Другие сервисы мониторинга { #other-monitoring-services }
Чтобы отправлять телеметрию в другой сервис мониторинга, настройте эндпоинт, который принимает **OTLP**, протокол OpenTelemetry для отправки телеметрии. Используйте базовый HTTP/protobuf-эндпоинт сервиса.
Задайте эти переменные окружения, заменив пример URL на ваш эндпоинт:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` идентифицирует ваше приложение в сервисе мониторинга. Эндпоинт — это базовый URL для получения данных. Трейсы отправляются в `/v1/traces`, метрики — в `/v1/metrics`, а логи — в `/v1/logs` по этому URL.
Если ваш сервис требует аутентификации, задайте `OTEL_EXPORTER_OTLP_HEADERS` со значением HTTP-заголовков, которые он указывает, например `api-key=YOUR_API_KEY`.
## Запустите приложение { #run-the-app }
Запустите приложение в том же терминале:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
В другом терминале отправьте HTTP-запрос:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Откройте ваш сервис мониторинга и найдите `my-api`. После следующего экспорта вы сможете увидеть трейс со спаном `GET /items/{item_id}`, а также метрики количества HTTP-запросов, длительности ответа и активных HTTP-запросов.
## Настройте телеметрию { #customize-telemetry }
### Настройте провайдеры и экспортеры { #configure-providers-and-exporters }
**Провайдер** предоставляет объекты, которые записывают трейсы, метрики или логи. Его конфигурация управляет тем, как эти данные обрабатываются и экспортируются.
Библиотеки телеметрии могут настраивать глобальные провайдеры OpenTelemetry. Настройте библиотеку до запуска приложения, и FastAPI будет использовать эти провайдеры автоматически.
Когда OTLP-эндпоинт задан в окружении, FastAPI добавляет экспортер для этого получателя к каждому включённому провайдеру. Существующие экспортеры продолжают отправлять данные своим получателям.
Настраивайте каждого получателя один раз. Если другая библиотека уже отправляет данные получателю, заданному в окружении, отключите у неё экспорт по настройкам из окружения или выключите автоматическую настройку FastAPI:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Вы также можете передать провайдер напрямую в словаре `telemetry`. Например, этот провайдер использует консольный экспортер OpenTelemetry, чтобы печатать спаны HTTP-запросов в вашем терминале:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**Экспортер** отправляет спаны их получателю. `BatchSpanProcessor` группирует спаны и отправляет их в фоне. Замените консольный экспортер на экспортер, предоставленный вашей библиотекой мониторинга, чтобы использовать её получателя. См. [руководство OpenTelemetry по инструментированию Python](https://opentelemetry.io/docs/languages/python/instrumentation/) для дополнительных вариантов настройки.
Используйте `meter_provider` или `logger_provider` в том же словаре, чтобы предоставить провайдер метрик или логов. Приложение или библиотека, создающая провайдер, управляет его завершением работы. FastAPI управляет компонентами экспорта, которые он добавляет.
/// warning | Предупреждение
OpenTelemetry по умолчанию использует глобальные провайдеры. Независимая настройка телеметрии для [монтированных подприложений](sub-applications.md) не гарантируется.
///
### Трассировка операций HTTP-запроса { #trace-request-operations }
По умолчанию трейсы HTTP-запросов включают спаны для разрешения зависимостей, запуска вашей функции-обработчика пути, сериализации HTTP-ответа и запуска каждой задачи в `BackgroundTasks` FastAPI. Эти спаны используют тот же провайдер и экспортеры.
Спаны фоновых задач остаются частью трейса HTTP-запроса. Они выполняются после завершения спана HTTP-ответа, поэтому не увеличивают измеренное время ответа.
Чтобы записывать только спан HTTP-запроса, установите `operation_spans` в `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Трассировка WebSocket-соединений { #trace-websocket-connections }
У каждого WebSocket-соединения есть спан, например `WS /ws/{room}`, охватывающий обработчик и очистку зависимостей. Он использует тех же провайдеров и те же настройки, включая `operation_spans` для разрешения зависимостей и выполнения эндпоинта.
Метрики HTTP-запросов охватывают только HTTP-запросы. Обычные отключения WebSocket с кодами `1000` или `1001` не создают логи ошибок.
### Анализ ошибок { #inspect-errors }
FastAPI записывает необработанные исключения как логи OpenTelemetry, связанные с трейсом HTTP-запроса или соединения. Логи ошибок записываются даже тогда, когда трейс не попал в выборку.
Логи исключений включают тип исключения, сообщение и трассировку стека. Сообщения и трассировки стека могут содержать конфиденциальную информацию. Используйте процессоры логов вашего провайдера, чтобы фильтровать или редактировать их, либо установите `logs` в `False`, чтобы отключить эти логи.
FastAPI также записывает ошибки валидации HTTP-запросов как логи уровня предупреждения с маршрутом и количеством ошибок. Эти логи не включают невалидные входные данные.
## Выберите, что записывать { #choose-what-to-record }
Словарь `telemetry` также принимает следующие настройки:
| Настройка | Назначение | По умолчанию |
| --- | --- | --- |
| `tracing` | Записывать спаны HTTP-запросов и WebSocket-соединений | `True` |
| `metrics` | Записывать метрики HTTP-запросов | `True` |
| `logs` | Записывать ошибки валидации и необработанные исключения | `True` |
| `operation_spans` | Добавлять спаны для операций HTTP-запроса | `True` |
| `exclude` | Пропускать HTTP-запросы, когда функция, получающая ASGI scope, возвращает `True` | `None` |
| `auto_configure` | Добавлять экспортеры для эндпоинтов, заданных в переменных окружения | `True` |
Например, чтобы собирать метрики, исключая проверки работоспособности:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Установите `auto_configure` в `False`, когда ваше приложение самостоятельно выполняет настройку провайдера, например внутри своей lifespan-функции.
+11
View File
@@ -8,6 +8,8 @@ Use a neutral tone (not overly formal or informal).
Use correct Russian grammar — appropriate cases, suffixes, and endings depending on context.
Translate technical terms in prose unless explicitly instructed otherwise below. If there is no commonly used Russian equivalent, use a Russified English term, optionally adding a short Russian explanation. Preserve code identifiers, API names, and protocol names.
For the following technical terms, use these specific translations to ensure consistency and clarity across the documentation:
* production (meaning production software or environment): продакшн (do not change the ending, for example, translate `in production` as `в продакшн` (not `в продакшене`))
@@ -48,6 +50,15 @@ For the following technical terms, use these specific translations to ensure con
* media type: тип содержимого (or `медиа-тип`)
* request: HTTP-запрос
* response: HTTP-ответ
* endpoint: эндпоинт (for example, `OTLP-эндпоинт`)
* handler: обработчик
* span (in tracing): спан (add `отрезок трассировки` if clarification is needed)
* stack trace: трассировка стека
* warning logs: логи уровня предупреждения
* destination (meaning a telemetry destination): получатель (for example, `отправлять данные получателю`, not `отправлять данные в назначение`)
* trace is not sampled: трейс не попал в выборку
* sensitive information: конфиденциальная информация
* health check: проверка работоспособности (or `проверка состояния`)
* type hints: аннотации типов
* type annotations: аннотации типов
* context manager: менеджер контекста
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
API'niz çalışırken ne kadar trafik aldığını, hangi request'lerin yavaş olduğunu ve hataların ne zaman oluştuğunu bilmek isteyebilirsiniz.
**Telemetri**, uygulamanızın davranışı hakkında bu soruları yanıtlamanıza yardımcı olan verilerdir. Yaygın türleri şunlardır:
- **Metrikler**: Response süreleri ve işlenen request sayısı gibi zaman içinde özetleyebileceğiniz ölçümler.
- **Trace'ler**: Tek tek request'lerin ve bunları işlemek için gerçekleştirilen işlemlerin kayıtları. Süresi ölçülen her işleme **span** denir.
- **Log'lar**: Uygulamanın başlatılması veya bir işlemin başarısız olması gibi olayların zaman damgalı kayıtları.
[**OpenTelemetry**](https://opentelemetry.io/), telemetri verilerini toplamak ve bunları panolarda inceleyebileceğiniz bir izleme hizmetine göndermek için kullanılan standartlar ve araçlar bütünüdür.
**FastAPI, HTTP request trace'leri, metrikleri ve log'ları için varsayılan olarak OpenTelemetry desteği sunar**. WebSocket bağlantıları da trace ve log üretir. Bu verileri görmek için onları alacak bir izleme hizmeti yapılandırın.
## FastAPI'yi Yükleyin { #install-fastapi }
FastAPI'yi, telemetri göndermek için gereken paketleri içeren `standard` ek bağımlılıklarıyla yükleyin:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Uygulamayı Oluşturun { #create-the-app }
`main.py` adlı bir dosya oluşturun:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Her şeyin varsayılan olarak çalıştığına dikkat edin; telemetrinin çalışması için özel bir kod yazmanız gerekmez.
## FastAPI Cloud { #fastapi-cloud }
Uygulamanızı `fastapi[standard]` ile [FastAPI Cloud](https://fastapicloud.com)'a dağıttığınızda metrikler otomatik olarak çalışır. Başka bir yapılandırma yapmanız gerekmez.
Pro planlarında request sayılarını, hata oranlarını ve response sürelerini [Metrikler panosunda](https://fastapicloud.com/docs/monitoring-and-performance/metrics/) görüntüleyebilirsiniz.
<img src="/img/tutorial/opentelemetry/image01.png" alt="Örnek veriler içeren FastAPI Cloud Pro metrikler panosu">
## Diğer İzleme Hizmetleri { #other-monitoring-services }
Telemetri verilerini başka bir izleme hizmetine göndermek için OpenTelemetry'nin telemetri gönderme protokolü olan **OTLP**'yi kabul eden bir endpoint yapılandırın. Hizmetin HTTP/protobuf temel endpoint'ini kullanın.
Örnek URL'yi kendi endpoint'inizle değiştirerek şu ortam değişkenlerini ayarlayın:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME`, izleme hizmetinde uygulamanızı tanımlar. Endpoint, verilerin alınacağı temel URL'dir. Trace'ler bu URL altındaki `/v1/traces` adresine, metrikler `/v1/metrics` adresine ve log'lar `/v1/logs` adresine gönderilir.
Hizmetiniz kimlik doğrulaması gerektiriyorsa `OTEL_EXPORTER_OTLP_HEADERS` değerini, hizmetin belirttiği header'lara göre ayarlayın. Örneğin: `api-key=YOUR_API_KEY`.
## Uygulamayı Çalıştırın { #run-the-app }
Uygulamayı aynı terminalde başlatın:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
Başka bir terminalden bir request gönderin:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
İzleme hizmetinizi açıp `my-api` uygulamasını bulun. Bir sonraki veri aktarımından sonra `GET /items/{item_id}` span'i içeren bir trace ile birlikte request sayısı, response süresi ve etkin request metriklerini görebilirsiniz.
## Telemetriyi Özelleştirin { #customize-telemetry }
### Provider ve Exporter'ları Yapılandırın { #configure-providers-and-exporters }
**Provider** (sağlayıcı), trace, metrik veya log kaydeden nesneleri sağlar. Yapılandırması, bu verilerin nasıl işleneceğini ve dışa aktarılacağını belirler.
Telemetri kütüphaneleri, OpenTelemetry'nin global provider'larını yapılandırabilir. Kütüphaneyi uygulama başlamadan önce yapılandırın; FastAPI bu provider'ları otomatik olarak kullanır.
Ortam değişkenlerinde bir OTLP endpoint'i ayarlandığında FastAPI, etkin olan her provider'a bu hedef için bir exporter ekler. Mevcut exporter'lar kendi hedeflerine veri göndermeye devam eder.
Her hedefi yalnızca bir kez yapılandırın. Ortam değişkenlerinde belirtilen hedefe gönderimi zaten başka bir kütüphane yönetiyorsa o kütüphanenin bu hedefe veri aktarımını devre dışı bırakın veya FastAPI'nin otomatik yapılandırmasını kapatın:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Ayrıca `telemetry` sözlüğünde doğrudan bir provider da iletebilirsiniz. Örneğin aşağıdaki provider, request span'lerini terminalinize yazdırmak için OpenTelemetry'nin konsol exporter'ını kullanır:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**Exporter** (dışa aktarıcı), span'leri hedeflerine gönderir. `BatchSpanProcessor`, span'leri gruplandırıp arka planda gönderir. İzleme kütüphanenizin hedefini kullanmak için konsol exporter'ını, o kütüphanenin sağladığı bir exporter ile değiştirin. Daha fazla yapılandırma seçeneği için [OpenTelemetry'nin Python enstrümantasyon rehberine](https://opentelemetry.io/docs/languages/python/instrumentation/) bakın.
Bir metrik veya log provider'ı sağlamak için aynı sözlükte `meter_provider` ya da `logger_provider` kullanın. Provider'ın kapatılmasını, onu oluşturan uygulama veya kütüphane yönetir. FastAPI ise kendisinin eklediği dışa aktarma bileşenlerini yönetir.
/// warning | Uyarı
OpenTelemetry varsayılan olarak global provider'ları kullanır. [Bağlanan alt uygulamalar](sub-applications.md) için bağımsız telemetri yapılandırması garanti edilmez.
///
### Request İşlemlerini İzleyin { #trace-request-operations }
Varsayılan olarak request trace'leri; bağımlılıkların çözümlenmesi, path işlemi fonksiyonunuzun çalıştırılması, response'un serileştirilmesi ve FastAPI'nin `BackgroundTasks` sınıfındaki her görevin çalıştırılması için span'ler içerir. Bu span'ler aynı provider ve exporter'ları kullanır.
Arka plan görevlerinin span'leri, request'in trace'inin bir parçası olmaya devam eder. HTTP response span'i bittikten sonra çalıştıkları için ölçülen response süresini artırmazlar.
Yalnızca HTTP request span'ini kaydetmek için `operation_spans` değerini `False` olarak ayarlayın:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### WebSocket Bağlantılarını İzleyin { #trace-websocket-connections }
Her WebSocket bağlantısının, handler'ı ve bağımlılık temizliğini kapsayan `WS /ws/{room}` gibi bir span'i vardır. Bağımlılık çözümleme ve endpoint'in çalıştırılması için kullanılan `operation_spans` dahil olmak üzere aynı provider'ları ve ayarları kullanır.
HTTP request metrikleri yalnızca HTTP request'lerini kapsar. `1000` veya `1001` kodlarıyla normal şekilde sonlanan WebSocket bağlantıları hata log'u üretmez.
### Hataları İnceleyin { #inspect-errors }
FastAPI, yakalanmamış istisnaları request'in veya bağlantının trace'iyle ilişkilendirilmiş OpenTelemetry log'ları olarak kaydeder. Trace örneklemeye dahil edilmese bile hata log'ları kaydedilir.
İstisna log'ları; istisnanın türünü, mesajını ve stack trace'ini içerir. Mesajlar ve stack trace'ler hassas bilgiler içerebilir. Bunları filtrelemek veya hassas bilgileri maskelemek için provider'ınızın log işlemcilerini kullanın ya da bu log'ları devre dışı bırakmak için `logs` değerini `False` olarak ayarlayın.
FastAPI, request doğrulama hatalarını da route ve hata sayısını içeren uyarı log'ları olarak kaydeder. Bu log'lar geçersiz girdiyi içermez.
## Nelerin Kaydedileceğini Seçin { #choose-what-to-record }
`telemetry` sözlüğü şu ayarları da kabul eder:
| Ayar | Amaç | Varsayılan |
| --- | --- | --- |
| `tracing` | HTTP request ve WebSocket bağlantı span'lerini kaydetmek | `True` |
| `metrics` | HTTP request metriklerini kaydetmek | `True` |
| `logs` | Doğrulama hatalarını ve yakalanmamış istisnaları kaydetmek | `True` |
| `operation_spans` | Request işlemleri için span eklemek | `True` |
| `exclude` | ASGI scope'unu alan bir fonksiyon `True` döndürdüğünde request'leri atlamak | `None` |
| `auto_configure` | Ortam değişkenlerinde ayarlanan endpoint'ler için exporter eklemek | `True` |
Örneğin, sağlık kontrollerini hariç tutarak metrik toplamak için:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Uygulamanız provider kurulumunu kendisi yönetiyorsa, örneğin lifespan fonksiyonu içinde yapıyorsa, `auto_configure` değerini `False` olarak ayarlayın.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
Коли ваш API працює, вам може бути потрібно знати, скільки трафіку він отримує, які запити повільні та коли виникають помилки.
**Телеметрія** - це дані про поведінку вашого застосунку, які допомагають відповісти на ці запитання. Поширені типи:
- **Метрики**: вимірювання, які можна узагальнювати за певний час, як-от час відповіді та кількість оброблюваних запитів.
- **Трасування**: записи окремих запитів і операцій, виконаних для їх обробки. Кожна операція з виміряною тривалістю називається **span**.
- **Журнали**: записи подій із часовими мітками, як-от запуск застосунку або збій операції.
[**OpenTelemetry**](https://opentelemetry.io/) - це набір стандартів та інструментів для збирання телеметрії та надсилання її до сервісу моніторингу, де ви можете переглядати її на інформаційних панелях.
**FastAPI надає підтримку OpenTelemetry за замовчуванням** для трасувань, метрик і журналів HTTP-запитів. З'єднання WebSocket також надають трасування та журнали. Щоб переглядати ці дані, налаштуйте сервіс моніторингу для їх отримання.
## Встановіть FastAPI { #install-fastapi }
Встановіть FastAPI з додатковими залежностями `standard`, які включають пакети для надсилання телеметрії:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## Створіть застосунок { #create-the-app }
Створіть файл `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
Зверніть увагу: усе працює за замовчуванням, вам не потрібно писати власний код для роботи телеметрії.
## FastAPI Cloud { #fastapi-cloud }
Коли ви розгортаєте застосунок у [FastAPI Cloud](https://fastapicloud.com) із `fastapi[standard]`, метрики працюють автоматично. Вам не потрібно нічого додатково налаштовувати.
На тарифних планах Pro ви можете переглядати кількість запитів, частоту помилок і час відповіді на [панелі метрик](https://fastapicloud.com/docs/monitoring-and-performance/metrics/).
<img src="/img/tutorial/opentelemetry/image01.png" alt="Панель метрик FastAPI Cloud Pro з прикладами даних">
## Інші сервіси моніторингу { #other-monitoring-services }
Щоб надсилати телеметрію до іншого сервісу моніторингу, налаштуйте кінцеву точку, яка приймає **OTLP**, протокол OpenTelemetry для надсилання телеметрії. Використовуйте базову кінцеву точку HTTP/protobuf цього сервісу.
Задайте ці змінні оточення, замінивши URL із прикладу на вашу кінцеву точку:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` ідентифікує ваш застосунок у сервісі моніторингу. Кінцева точка - це базовий URL для отримання даних. Трасування надсилаються до `/v1/traces`, метрики - до `/v1/metrics`, а журнали - до `/v1/logs` за цим URL.
Якщо ваш сервіс вимагає автентифікації, задайте в `OTEL_EXPORTER_OTLP_HEADERS` заголовки, які він визначає, наприклад `api-key=YOUR_API_KEY`.
## Запустіть застосунок { #run-the-app }
Запустіть застосунок у тому самому терміналі:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
В іншому терміналі надішліть запит:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
Відкрийте ваш сервіс моніторингу та знайдіть `my-api`. Після наступного експорту ви зможете побачити трасування зі span `GET /items/{item_id}`, а також метрики кількості запитів, тривалості відповіді та активних запитів.
## Налаштуйте телеметрію { #customize-telemetry }
### Налаштуйте провайдери та експортери { #configure-providers-and-exporters }
**Провайдер** надає об'єкти, які записують трасування, метрики або журнали. Його конфігурація визначає, як ці дані обробляються та експортуються.
Бібліотеки телеметрії можуть налаштовувати глобальні провайдери OpenTelemetry. Налаштуйте бібліотеку перед запуском застосунку, і FastAPI автоматично використовуватиме ці провайдери.
Коли кінцеву точку OTLP задано в оточенні, FastAPI додає експортер для цього місця призначення до кожного ввімкненого провайдера. Наявні експортери продовжують надсилати дані до своїх місць призначення.
Налаштовуйте кожне місце призначення лише один раз. Якщо інша бібліотека вже обробляє місце призначення, задане в оточенні, вимкніть у ній експорт за налаштуваннями оточення або вимкніть автоматичне налаштування FastAPI:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
Ви також можете передати провайдер безпосередньо у словнику `telemetry`. Наприклад, цей провайдер використовує консольний експортер OpenTelemetry для виведення span запитів у вашому терміналі:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**Експортер** надсилає span до їхнього місця призначення. `BatchSpanProcessor` групує span і надсилає їх у фоновому режимі. Замініть консольний експортер на той, який надає ваша бібліотека моніторингу, щоб використовувати її місце призначення. Додаткові параметри налаштування наведено в [посібнику з інструментування Python для OpenTelemetry](https://opentelemetry.io/docs/languages/python/instrumentation/).
Використовуйте `meter_provider` або `logger_provider` у тому самому словнику, щоб надати провайдер метрик або журналів. Застосунок або бібліотека, що створює провайдер, керує його вимкненням. FastAPI керує компонентами експорту, які додає.
/// warning | Попередження
OpenTelemetry за замовчуванням використовує глобальні провайдери. Незалежне налаштування телеметрії для [змонтованих підзастосунків](sub-applications.md) не гарантоване.
///
### Трасуйте операції запитів { #trace-request-operations }
За замовчуванням трасування запитів включають span для розв'язання залежностей, виконання вашої функції операції шляху, серіалізації відповіді та виконання кожного завдання у `BackgroundTasks` FastAPI. Ці span використовують ті самі провайдер і експортери.
Span фонових завдань залишаються частиною трасування запиту. Вони виконуються після завершення span HTTP-відповіді, тому не збільшують виміряний час відповіді.
Щоб записувати лише span HTTP-запиту, задайте `operation_spans` значення `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### Трасуйте з'єднання WebSocket { #trace-websocket-connections }
Кожне з'єднання WebSocket має span, наприклад `WS /ws/{room}`, що охоплює обробник і очищення залежностей. Він використовує ті самі провайдери й налаштування, включно з `operation_spans` для розв'язання залежностей і виконання кінцевої точки.
Метрики HTTP-запитів охоплюють лише HTTP-запити. Звичайні від'єднання WebSocket із кодами `1000` або `1001` не створюють записів про помилки в журналах.
### Переглядайте помилки { #inspect-errors }
FastAPI записує необроблені винятки як журнали OpenTelemetry, пов'язані з трасуванням запиту або з'єднання. Помилки записуються в журнали, навіть коли трасування не потрапляє до вибірки.
Записи винятків у журналах містять тип винятку, повідомлення та трасування стека. Повідомлення та трасування стека можуть містити конфіденційну інформацію. Використовуйте процесори журналів вашого провайдера для фільтрування або приховування такої інформації або задайте `logs` значення `False`, щоб вимкнути ці журнали.
FastAPI також записує помилки валідації запитів як попередження в журналах із маршрутом і кількістю помилок. Ці записи не містять некоректних вхідних даних.
## Виберіть, що записувати { #choose-what-to-record }
Словник `telemetry` також приймає такі налаштування:
| Налаштування | Призначення | За замовчуванням |
| --- | --- | --- |
| `tracing` | Записувати span HTTP-запитів і з'єднань WebSocket | `True` |
| `metrics` | Записувати метрики HTTP-запитів | `True` |
| `logs` | Записувати помилки валідації та необроблені винятки | `True` |
| `operation_spans` | Додавати span для операцій запитів | `True` |
| `exclude` | Пропускати запити, коли функція, що отримує ASGI scope, повертає `True` | `None` |
| `auto_configure` | Додавати експортери для кінцевих точок, заданих у змінних оточення | `True` |
Наприклад, щоб збирати метрики, виключаючи перевірки працездатності:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
Задайте `auto_configure` значення `False`, коли ваш застосунок самостійно налаштовує провайдери, наприклад у своїй функції тривалості життя.
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
當你的 API 運作時,你可能會想知道它收到多少流量、哪些請求較慢,以及何時發生錯誤。
**遙測(Telemetry)**是關於應用程式行為的資料,能協助你回答這些問題。常見類型包括:
- **指標(Metrics)**:可隨時間彙整的測量值,例如回應時間和正在處理的請求數量。
- **追蹤(Traces)**:個別請求及其處理過程中所執行操作的記錄。每個計時的操作稱為一個 **span**。
- **日誌(Logs)**:附有時間戳記的事件記錄,例如應用程式啟動或操作失敗。
[**OpenTelemetry**](https://opentelemetry.io/) 是一套標準與工具,用於收集遙測資料並將其傳送到監控服務,讓你能在儀表板中查看這些資料。
**FastAPI 預設提供 OpenTelemetry 支援**,涵蓋 HTTP 請求的追蹤、指標和日誌。WebSocket 連線也提供追蹤和日誌。若要查看這些資料,請設定監控服務來接收它們。
## 安裝 FastAPI { #install-fastapi }
安裝 FastAPI 時加上 `standard` 額外選項,其中包含傳送遙測資料所需的套件:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## 建立應用程式 { #create-the-app }
建立一個 `main.py` 檔案:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
請注意,這些功能預設就能運作,你不需要撰寫任何自訂程式碼來啟用遙測。
## FastAPI Cloud { #fastapi-cloud }
當你使用 `fastapi[standard]` 部署到 [FastAPI Cloud](https://fastapicloud.com) 時,指標功能會自動運作。你不需要做任何額外設定。
使用 Pro 方案時,你可以在[指標儀表板](https://fastapicloud.com/docs/monitoring-and-performance/metrics/)中查看請求數量、錯誤率和回應時間。
<img src="/img/tutorial/opentelemetry/image01.png" alt="含有範例資料的 FastAPI Cloud Pro 指標儀表板">
## 其他監控服務 { #other-monitoring-services }
若要將遙測資料傳送到其他監控服務,請設定一個接受 **OTLP** 的端點。OTLP 是 OpenTelemetry 用於傳送遙測資料的協定。請使用該服務的 HTTP/protobuf 基礎端點。
設定以下環境變數,並將範例 URL 替換為你的端點:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` 用於在監控服務中識別你的應用程式。端點是接收資料的基礎 URL。追蹤會傳送到該 URL 下的 `/v1/traces`,指標傳送到 `/v1/metrics`,日誌則傳送到 `/v1/logs`。
如果你的服務需要驗證身分,請將 `OTEL_EXPORTER_OTLP_HEADERS` 設定為該服務指定的標頭,例如 `api-key=YOUR_API_KEY`。
## 執行應用程式 { #run-the-app }
在同一個終端機中啟動應用程式:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
在另一個終端機中傳送請求:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
開啟你的監控服務並找到 `my-api`。下一次匯出後,你就能看到包含 `GET /items/{item_id}` span 的追蹤,以及請求數量、回應時間和處理中請求的指標。
## 自訂遙測 { #customize-telemetry }
### 設定提供者和匯出器 { #configure-providers-and-exporters }
**提供者(provider)**提供用於記錄追蹤、指標或日誌的物件。其設定控制這些資料的處理與匯出方式。
遙測函式庫可以設定 OpenTelemetry 的全域提供者。在應用程式啟動前設定好函式庫,FastAPI 就會自動使用這些提供者。
當環境中設定了 OTLP 端點時,FastAPI 會為每個已啟用的提供者新增一個指向該目的地的匯出器。現有的匯出器會繼續將資料傳送到各自的目的地。
每個目的地只需設定一次。如果另一個函式庫已經負責處理環境變數指定的目的地,請停用該函式庫依環境變數進行匯出的功能,或關閉 FastAPI 的自動設定:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
你也可以直接在 `telemetry` 字典中傳入提供者。例如,這個提供者使用 OpenTelemetry 的主控台匯出器,在終端機中印出請求的 span:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**匯出器(exporter)**會將 span 傳送到目的地。`BatchSpanProcessor` 會將 span 分組,並在背景傳送。將主控台匯出器替換成監控函式庫提供的匯出器,即可使用該函式庫的目的地。更多設定選項請參閱 [OpenTelemetry 的 Python instrumentation 指南](https://opentelemetry.io/docs/languages/python/instrumentation/)。
在同一個字典中使用 `meter_provider` 或 `logger_provider`,即可提供指標或日誌的提供者。建立提供者的應用程式或函式庫負責管理其關閉流程。FastAPI 則管理它所新增的匯出元件。
/// warning | 警告
OpenTelemetry 預設使用全域提供者。不保證[掛載的子應用程式](sub-applications.md)能使用獨立的遙測設定。
///
### 追蹤請求操作 { #trace-request-operations }
預設情況下,請求追蹤會包含解析相依性、執行路徑操作函式、序列化回應,以及執行 FastAPI `BackgroundTasks` 中各個任務的 span。這些 span 使用相同的提供者和匯出器。
背景任務的 span 仍屬於該請求的追蹤。它們在 HTTP 回應 span 結束後才執行,因此不會增加測得的回應時間。
若只想記錄 HTTP 請求 span,請將 `operation_spans` 設為 `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### 追蹤 WebSocket 連線 { #trace-websocket-connections }
每個 WebSocket 連線都有一個 span,例如 `WS /ws/{room}`,涵蓋處理函式及相依性清理。它使用相同的提供者和設定,包括用於相依性解析和端點執行的 `operation_spans`。
HTTP 請求指標僅涵蓋 HTTP 請求。使用代碼 `1000` 或 `1001` 正常中斷的 WebSocket 連線不會產生錯誤日誌。
### 檢查錯誤 { #inspect-errors }
FastAPI 會將未處理的例外記錄為 OpenTelemetry 日誌,並連結到該請求或連線的追蹤。即使追蹤未被取樣,仍會記錄錯誤日誌。
例外日誌包含例外的類型、訊息和堆疊追蹤。訊息與堆疊追蹤可能包含敏感資訊。請使用提供者的日誌處理器來篩選或遮蔽這些資訊,或將 `logs` 設為 `False` 以停用這些日誌。
FastAPI 也會將請求驗證失敗記錄為警告日誌,其中包含路由和錯誤數量。這些日誌不會包含無效的輸入。
## 選擇要記錄的內容 { #choose-what-to-record }
`telemetry` 字典也接受以下設定:
| 設定 | 用途 | 預設值 |
| --- | --- | --- |
| `tracing` | 記錄 HTTP 請求和 WebSocket 連線的 span | `True` |
| `metrics` | 記錄 HTTP 請求指標 | `True` |
| `logs` | 記錄驗證失敗和未處理的例外 | `True` |
| `operation_spans` | 為請求操作新增 span | `True` |
| `exclude` | 當接收 ASGI scope 的函式回傳 `True` 時,略過該請求 | `None` |
| `auto_configure` | 為環境變數中設定的端點新增匯出器 | `True` |
例如,若要收集指標,但排除健康檢查:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
當你的應用程式自行處理提供者設定時,例如在 lifespan 函式中設定,請將 `auto_configure` 設為 `False`。
+161
View File
@@ -0,0 +1,161 @@
# OpenTelemetry { #opentelemetry }
当你的 API 运行时,你可能想知道它接收了多少流量、哪些请求较慢,以及何时发生错误。
**遥测**是反映应用行为的数据,可以帮助你回答这些问题。常见类型包括:
- **指标(Metrics)**:可以按时间汇总的测量值,例如响应时间和正在处理的请求数。
- **追踪(Traces)**:记录单个请求及处理该请求所执行的操作。每个计时的操作称为一个 **span**。
- **日志(Logs)**:带有时间戳的事件记录,例如应用启动或操作失败。
[**OpenTelemetry**](https://opentelemetry.io/) 是一套用于收集遥测数据并将其发送到监控服务的标准和工具,你可以在监控服务的仪表板中查看这些数据。
**FastAPI 默认提供 OpenTelemetry 支持**,用于 HTTP 请求的追踪、指标和日志。WebSocket 连接也提供追踪和日志。要查看这些数据,请配置监控服务来接收它们。
## 安装 FastAPI { #install-fastapi }
安装 FastAPI 时启用 `standard` 扩展依赖,其中包含发送遥测数据所需的包:
<div class="termy">
```console
$ uv add "fastapi[standard]"
---> 100%
```
</div>
## 创建应用 { #create-the-app }
创建文件 `main.py`:
{* ../../docs_src/opentelemetry/tutorial001_py310.py *}
注意,这一切默认就能工作,你无需编写任何自定义代码即可使用遥测功能。
## FastAPI Cloud { #fastapi-cloud }
使用 `fastapi[standard]` 部署到 [FastAPI Cloud](https://fastapicloud.com) 时,指标功能会自动工作。你无需进行其他配置。
使用 Pro 套餐时,你可以在[指标仪表板](https://fastapicloud.com/docs/monitoring-and-performance/metrics/)中查看请求数、错误率和响应时间。
<img src="/img/tutorial/opentelemetry/image01.png" alt="显示示例数据的 FastAPI Cloud Pro 指标仪表板">
## 其他监控服务 { #other-monitoring-services }
要将遥测数据发送到其他监控服务,请配置一个接受 **OTLP** 的端点,OTLP 是 OpenTelemetry 用于发送遥测数据的协议。请使用该服务的 HTTP/protobuf 基础端点。
设置以下环境变量,将示例 URL 替换为你的端点:
```bash
export OTEL_SERVICE_NAME=my-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
```
`OTEL_SERVICE_NAME` 用于在监控服务中标识你的应用。端点是接收数据的基础 URL。追踪会发送到该 URL 下的 `/v1/traces`,指标发送到 `/v1/metrics`,日志发送到 `/v1/logs`。
如果你的服务需要身份验证,请将 `OTEL_EXPORTER_OTLP_HEADERS` 设置为该服务指定的请求头,例如 `api-key=YOUR_API_KEY`。
## 运行应用 { #run-the-app }
在同一个终端中启动应用:
<div class="termy">
```console
$ uv run fastapi run
```
</div>
在另一个终端中发送请求:
```console
$ curl http://127.0.0.1:8000/items/1
{"item_id":1}
```
打开你的监控服务,找到 `my-api`。下一次导出后,你就能看到一条包含 `GET /items/{item_id}` span 的追踪,以及请求数、响应耗时和活跃请求数的指标。
## 自定义遥测 { #customize-telemetry }
### 配置 provider 和 exporter { #configure-providers-and-exporters }
**provider** 提供用于记录追踪、指标或日志的对象。其配置决定了这些数据的处理和导出方式。
遥测库可以配置 OpenTelemetry 的全局 provider。在应用启动之前配置好该库,FastAPI 就会自动使用这些 provider。
当环境中设置了 OTLP 端点时,FastAPI 会为每个已启用的 provider 添加一个向该目标发送数据的 exporter。现有的 exporter 会继续向各自的目标发送数据。
每个目标只需配置一次。如果另一个库已经负责向环境变量指定的目标导出数据,请禁用该库基于环境变量的导出功能,或关闭 FastAPI 的自动配置:
```python
app = FastAPI(telemetry={"auto_configure": False})
```
你也可以直接在 `telemetry` 字典中传入 provider。例如,下面的 provider 使用 OpenTelemetry 的控制台 exporter,在终端中打印请求 span:
{* ../../docs_src/opentelemetry/tutorial002_py310.py hl[2:8] *}
**exporter** 将 span 发送到目标位置。`BatchSpanProcessor` 将 span 分组,并在后台发送。要使用监控库的目标位置,请将控制台 exporter 替换为该库提供的 exporter。更多配置选项请参阅 [OpenTelemetry 的 Python 插桩指南](https://opentelemetry.io/docs/languages/python/instrumentation/)。
在同一个字典中使用 `meter_provider` 或 `logger_provider` 来提供指标或日志 provider。创建 provider 的应用或库负责管理其关闭过程。FastAPI 负责管理它所添加的导出组件。
/// warning | 警告
OpenTelemetry 默认使用全局 provider。不保证[挂载的子应用](sub-applications.md)可以拥有独立的遥测配置。
///
### 追踪请求操作 { #trace-request-operations }
默认情况下,请求追踪包含以下操作的 span:解析依赖项、运行路径操作函数、序列化响应,以及运行 FastAPI 的 `BackgroundTasks` 中的每个任务。这些 span 使用相同的 provider 和 exporter。
后台任务的 span 仍属于该请求的追踪。它们在 HTTP 响应 span 结束后运行,因此不会增加测得的响应时间。
要仅记录 HTTP 请求 span,请将 `operation_spans` 设置为 `False`:
{* ../../docs_src/opentelemetry/tutorial003_py310.py hl[3] *}
### 追踪 WebSocket 连接 { #trace-websocket-connections }
每个 WebSocket 连接都有一个类似 `WS /ws/{room}` 的 span,涵盖处理函数和依赖项清理过程。它使用相同的 provider 和设置,包括用于依赖项解析和端点执行的 `operation_spans`。
HTTP 请求指标仅涵盖 HTTP 请求。使用代码 `1000` 或 `1001` 正常断开的 WebSocket 连接不会产生日志错误。
### 检查错误 { #inspect-errors }
FastAPI 将未处理的异常记录为 OpenTelemetry 日志,并关联到请求或连接的追踪。即使追踪未被采样,也会记录错误日志。
异常日志包含异常类型、消息和堆栈追踪。消息和堆栈追踪可能包含敏感信息。请使用 provider 的日志处理器对其进行过滤或脱敏,或将 `logs` 设置为 `False` 来禁用这些日志。
FastAPI 还会将请求验证失败记录为警告日志,其中包含路由和错误数量。这些日志不包含无效的输入。
## 选择要记录的内容 { #choose-what-to-record }
`telemetry` 字典还接受以下设置:
| 设置 | 用途 | 默认值 |
| --- | --- | --- |
| `tracing` | 记录 HTTP 请求和 WebSocket 连接的 span | `True` |
| `metrics` | 记录 HTTP 请求指标 | `True` |
| `logs` | 记录验证失败和未处理的异常 | `True` |
| `operation_spans` | 为请求操作添加 span | `True` |
| `exclude` | 当接收 ASGI scope 的函数返回 `True` 时跳过请求 | `None` |
| `auto_configure` | 为环境变量中设置的端点添加 exporter | `True` |
例如,要收集指标,同时排除健康检查:
```python
from fastapi import FastAPI
app = FastAPI(
telemetry={
"tracing": False,
"exclude": lambda scope: scope["path"] == "/health",
}
)
```
当应用自行配置 provider 时,例如在其 lifespan 函数中配置,请将 `auto_configure` 设置为 `False`。
@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
@@ -0,0 +1,13 @@
from fastapi import FastAPI
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
app = FastAPI(telemetry={"tracer_provider": tracer_provider})
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI(telemetry={"operation_spans": False})
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
+11
View File
@@ -12,6 +12,7 @@ Official FastAPI skill to write code with best practices, keeping up to date wit
* Serve frontend apps: use `app.frontend()` or `router.frontend()` for built frontend assets; see [Serve Frontend Apps](#serve-frontend-apps).
* Server-Sent Events (SSE): use `response_class=EventSourceResponse` and `yield`; see [Streaming](#streaming-json-lines-sse-bytes) and [the streaming reference](references/streaming.md).
* JSON Lines and byte streaming: see [the streaming reference](references/streaming.md).
* OpenTelemetry: use FastAPI's native traces, metrics, and logs. See [OpenTelemetry](#opentelemetry).
* Dependencies: use `Annotated[..., Depends(...)]`; see [Dependency Injection](#dependency-injection) and [the dependency injection reference](references/dependencies.md) for `yield`, scopes, and class dependencies.
* Response models: prefer return types; use `response_model` when the public response schema differs from the internal return value; see [the response reference](references/responses.md).
* Pydantic models: do not use ellipsis or `RootModel`; see [the Pydantic reference](references/pydantic.md).
@@ -203,6 +204,16 @@ app.include_router(router)
`app.frontend()` and `router.frontend()` are low-priority routes: regular API routes are matched first, then frontend files and client-side routing fallbacks. Use this for single-page apps and built frontend assets instead of mounting `StaticFiles` manually.
## OpenTelemetry
Prefer FastAPI's native OpenTelemetry support for request traces, metrics, and logs.
Install `fastapi[standard]` to include the SDK and HTTP/protobuf exporters. Set `OTEL_SERVICE_NAME` to identify the app and `OTEL_EXPORTER_OTLP_ENDPOINT` to the collector's HTTP/protobuf base URL. Use `OTEL_EXPORTER_OTLP_HEADERS` when authentication is required.
Use `FastAPI(telemetry={...})` for custom configuration, such as choosing signals or supplying providers.
See the [OpenTelemetry tutorial](https://fastapi.tiangolo.com/advanced/opentelemetry/) for configuration details.
## Dependency Injection
Use dependencies when the logic can't be declared in Pydantic validation, depends on external resources, needs cleanup with `yield`, or is shared across endpoints.
@@ -55,6 +55,7 @@ def get_username():
finally:
print("Clean up before response is sent")
UserNameDep = Annotated[str, Depends(get_username, scope="function")]
@app.get("/users/me")
@@ -18,6 +18,8 @@ When needing to run blocking code inside of async functions, or async code insid
Prefer it over AnyIO or asyncio.
Prefer it over `run_in_threadpool()`.
Install:
```bash
@@ -76,6 +76,7 @@ app = FastAPI()
class PNGStreamingResponse(StreamingResponse):
media_type = "image/png"
@app.get("/image", response_class=PNGStreamingResponse)
def stream_image_no_async_no_annotation():
with read_image() as image_file:
+1 -1
View File
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.141.1"
__version__ = "0.142.2"
from starlette import status as status
+65 -2
View File
@@ -21,6 +21,12 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.telemetry import TelemetryConfig
from fastapi.telemetry._asgi import (
ExceptionTelemetryMiddleware,
NativeTelemetry,
_legacy_otel,
)
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
@@ -58,6 +64,19 @@ class FastAPI(Starlette):
def __init__(
self: AppType,
*,
telemetry: Annotated[
TelemetryConfig | None,
Doc(
"""
Native OpenTelemetry configuration as a dictionary. Uses global
providers by default. Omitted options keep their defaults.
```python
app = FastAPI(telemetry={"tracing": False})
```
"""
),
] = None,
debug: Annotated[
bool,
Doc(
@@ -1011,6 +1030,20 @@ class FastAPI(Starlette):
websocket_request_validation_exception_handler, # type: ignore[arg-type]
) # ty: ignore[no-matching-overload]
self._telemetry: TelemetryConfig = {
"tracer_provider": None,
"meter_provider": None,
"logger_provider": None,
"tracing": True,
"metrics": True,
"logs": True,
"operation_spans": True,
"auto_configure": True,
"exclude": None,
**(telemetry if telemetry is not None else {}),
}
self._native_telemetry = NativeTelemetry(self._telemetry)
self.user_middleware: list[Middleware] = (
[] if middleware is None else list(middleware)
)
@@ -1031,7 +1064,10 @@ class FastAPI(Starlette):
exception_handlers[key] = value
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
[
Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug),
Middleware(ExceptionTelemetryMiddleware),
]
+ self.user_middleware
+ [
Middleware(
@@ -1160,7 +1196,34 @@ class FastAPI(Starlette):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.root_path:
scope["root_path"] = self.root_path
await super().__call__(scope, receive, send)
if scope["type"] == "lifespan":
from fastapi.telemetry._runtime import lifespan
await lifespan(
config=self._telemetry,
app=super().__call__,
scope=scope,
receive=receive,
send=send,
)
return
if (
scope["type"] not in ("http", "websocket")
or "fastapi.telemetry" in scope
or not self._native_telemetry.enabled()
):
await super().__call__(scope, receive, send)
return
if self.middleware_stack is None:
self.middleware_stack = self.build_middleware_stack()
scope["app"] = self
await self._native_telemetry(
app=super().__call__,
scope=scope,
receive=receive,
send=send,
legacy_otel=_legacy_otel(self.middleware_stack),
)
def add_api_route(
self,
+6
View File
@@ -2,6 +2,7 @@ from collections.abc import Callable
from typing import Annotated, Any
from annotated_doc import Doc
from fastapi.telemetry._api import _operation
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
from typing_extensions import ParamSpec
@@ -59,3 +60,8 @@ class BackgroundTasks(StarletteBackgroundTasks):
[FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/).
"""
return super().add_task(func, *args, **kwargs)
async def __call__(self) -> None:
for task in self.tasks:
with _operation(name="background_task", function=task.func):
await task()
+118 -38
View File
@@ -81,6 +81,13 @@ from fastapi.sse import (
ServerSentEvent,
format_sse_event,
)
from fastapi.telemetry._api import (
_operation,
_route_selected,
_run_sync_endpoint,
_validation_failed,
get_telemetry_data,
)
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import (
create_model_field,
@@ -349,9 +356,12 @@ async def run_endpoint_function(
assert dependant.call is not None, "dependant.call must be a function"
if is_coroutine:
return await dependant.call(**values)
with _operation(name="endpoint", function=dependant.call):
return await dependant.call(**values)
else:
return await run_in_threadpool(dependant.call, **values)
return await run_in_threadpool(
_run_sync_endpoint, function=dependant.call, arguments=values
)
def _build_response_args(
@@ -404,6 +414,9 @@ def get_request_handler(
actual_strict_content_type = strict_content_type
async def app(request: Request) -> Response:
telemetry_data = get_telemetry_data()
if telemetry_data is not None:
telemetry_data.request = request
response: Response | None = None
file_stack = request.scope.get("fastapi_middleware_astack")
assert isinstance(file_stack, AsyncExitStack), (
@@ -462,6 +475,7 @@ def get_request_handler(
body=e.doc,
endpoint_ctx=endpoint_ctx,
)
_validation_failed(validation_error)
raise validation_error from e
except HTTPException:
# If a middleware raises an HTTPException, it should be raised again
@@ -472,20 +486,27 @@ def get_request_handler(
)
raise http_error from e
if telemetry_data is not None:
telemetry_data.body = body
# Solve dependencies and run path operation function, auto-closing dependencies
errors: list[Any] = []
async_exit_stack = request.scope.get("fastapi_inner_astack")
assert isinstance(async_exit_stack, AsyncExitStack), (
"fastapi_inner_astack not found in request scope"
)
solved_result = await solve_dependencies(
request=request,
dependant=dependant,
body=cast(dict[str, Any] | FormData | bytes | None, body),
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
)
with _operation(name="dependencies", function=dependant.call):
solved_result = await solve_dependencies(
request=request,
dependant=dependant,
body=cast(dict[str, Any] | FormData | bytes | None, body),
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
)
if telemetry_data is not None:
telemetry_data.values = solved_result.values
telemetry_data.errors = solved_result.errors
errors = solved_result.errors
assert dependant.call # For types
if not errors:
@@ -724,19 +745,20 @@ def get_request_handler(
use_dump_json = response_field is not None and isinstance(
response_class, DefaultPlaceholder
)
content = await serialize_response(
field=response_field,
response_content=raw_response,
include=response_model_include,
exclude=response_model_exclude,
by_alias=response_model_by_alias,
exclude_unset=response_model_exclude_unset,
exclude_defaults=response_model_exclude_defaults,
exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine,
endpoint_ctx=endpoint_ctx,
dump_json=use_dump_json,
)
with _operation(name="serialization", function=dependant.call):
content = await serialize_response(
field=response_field,
response_content=raw_response,
include=response_model_include,
exclude=response_model_exclude,
by_alias=response_model_by_alias,
exclude_unset=response_model_exclude_unset,
exclude_defaults=response_model_exclude_defaults,
exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine,
endpoint_ctx=endpoint_ctx,
dump_json=use_dump_json,
)
if use_dump_json:
response = Response(
content=content,
@@ -752,6 +774,7 @@ def get_request_handler(
validation_error = RequestValidationError(
errors, body=body, endpoint_ctx=endpoint_ctx
)
_validation_failed(validation_error)
raise validation_error
# Return response
@@ -767,6 +790,9 @@ def get_websocket_app(
embed_body_fields: bool = False,
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
async def app(websocket: WebSocket) -> None:
telemetry_data = get_telemetry_data()
if telemetry_data is not None:
telemetry_data.websocket = websocket
endpoint_ctx = (
_extract_endpoint_context(dependant.call)
if dependant.call
@@ -780,20 +806,27 @@ def get_websocket_app(
assert isinstance(async_exit_stack, AsyncExitStack), (
"fastapi_inner_astack not found in request scope"
)
solved_result = await solve_dependencies(
request=websocket,
dependant=dependant,
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
)
with _operation(name="dependencies", function=dependant.call):
solved_result = await solve_dependencies(
request=websocket,
dependant=dependant,
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
)
if telemetry_data is not None:
telemetry_data.values = solved_result.values
telemetry_data.errors = solved_result.errors
if solved_result.errors:
raise WebSocketRequestValidationError(
validation_error = WebSocketRequestValidationError(
solved_result.errors,
endpoint_ctx=endpoint_ctx,
)
_validation_failed(validation_error)
raise validation_error
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**solved_result.values)
with _operation(name="endpoint", function=dependant.call):
await dependant.call(**solved_result.values)
return app
@@ -1271,12 +1304,8 @@ class APIRoute(routing.Route):
)
await response(scope, receive, send)
return
token = _effective_route_context_var.set(effective_context)
try:
app = request_response(self.get_route_handler())
finally:
_effective_route_context_var.reset(token)
await app(scope, receive, send)
assert effective_context.app is not None
await effective_context.app(scope, receive, send)
return
await super().handle(scope, receive, send)
@@ -1374,6 +1403,7 @@ class _RouterIncludeContext:
class _EffectiveRouteContext:
original_route: BaseRoute
starlette_route: BaseRoute | None = None
app: ASGIApp | None = field(default=None, repr=False, compare=False)
frontend_prefix: str = ""
path: str = ""
endpoint: Callable[..., Any] | None = None
@@ -1475,6 +1505,13 @@ class _EffectiveRouteContext:
),
stream_item_type=route.stream_item_type,
)
# Build once per inclusion context, just as APIRoute does for direct routes.
# Integrations may wrap dependant.call while constructing the handler.
token = _effective_route_context_var.set(context)
try:
context.app = request_response(original_route.get_route_handler())
finally:
_effective_route_context_var.reset(token)
return context
@classmethod
@@ -1791,6 +1828,12 @@ class _IncludedRouter(BaseRoute):
await route.handle(scope, receive, send)
return
if effective_context is not None:
_route_selected(
scope=scope,
path=getattr(effective_context.starlette_route, "path_format", None)
or effective_context.path_format,
mount=isinstance(route, routing.Mount),
)
_get_fastapi_scope(scope)[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = (
effective_context
)
@@ -2189,6 +2232,10 @@ class _FrontendRouteGroup(BaseRoute):
if match == Match.NONE or route is None:
raise HTTPException(status_code=404)
_update_scope(scope, child_scope)
_route_selected(
scope=scope,
path=_join_frontend_paths(prefix, route.path).rstrip("/") + "/{path}",
)
if match == Match.FULL and dependant and dependant.dependencies:
async with self._solve_dependencies(
scope,
@@ -2731,6 +2778,11 @@ class APIRouter(routing.Router):
match, child_scope = route.matches(scope)
if match == Match.FULL:
scope.update(child_scope)
_route_selected(
scope=scope,
path=getattr(route, "path_format", None),
mount=isinstance(route, routing.Mount),
)
await route.handle(scope, receive, send)
return
if match == Match.PARTIAL and partial is None:
@@ -2739,6 +2791,11 @@ class APIRouter(routing.Router):
if partial is not None:
route, child_scope = partial
scope.update(child_scope)
_route_selected(
scope=scope,
path=getattr(route, "path_format", None),
mount=isinstance(route, routing.Mount),
)
await route.handle(scope, receive, send)
return
@@ -2753,6 +2810,23 @@ class APIRouter(routing.Router):
for route in self.routes:
match, _ = route.matches(redirect_scope)
if match != Match.NONE:
if scope.get("fastapi.telemetry") is not None:
telemetry_route: BaseRoute | _EffectiveRouteContext | None = (
route
)
while isinstance(telemetry_route, _IncludedRouter):
_, _, matched_route, matched_context = (
telemetry_route._match(redirect_scope)
)
telemetry_route = (
matched_context.starlette_route or matched_context
if matched_context is not None
else matched_route
)
_route_selected(
scope=scope,
path=getattr(telemetry_route, "path_format", None),
)
redirect_url = URL(scope=redirect_scope)
response = RedirectResponse(url=str(redirect_url))
await response(scope, receive, send)
@@ -2766,6 +2840,12 @@ class APIRouter(routing.Router):
) = self._match_low_priority(scope)
if low_priority_match != Match.NONE and low_priority_route is not None:
_update_scope(scope, low_priority_scope)
_route_selected(
scope=scope,
path=getattr(
low_priority_context or low_priority_route, "path_format", None
),
)
if low_priority_context is not None:
_get_fastapi_scope(scope)[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = (
low_priority_context
+3
View File
@@ -0,0 +1,3 @@
from ._api import TelemetryConfig as TelemetryConfig
from ._api import TelemetryData as TelemetryData
from ._api import get_telemetry_data as get_telemetry_data
+276
View File
@@ -0,0 +1,276 @@
from collections.abc import Callable, Iterator, MutableMapping, Sequence
from contextlib import AbstractContextManager, contextmanager, nullcontext
from dataclasses import dataclass, field
from time import time_ns
from typing import Annotated, Any
from annotated_doc import Doc
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from opentelemetry import context as otel_context
from opentelemetry._logs import Logger, LoggerProvider, SeverityNumber
from opentelemetry.context import Context
from opentelemetry.metrics import MeterProvider
from opentelemetry.trace import Span, StatusCode, Tracer, TracerProvider
from starlette.exceptions import HTTPException, WebSocketException
from starlette.requests import Request
from starlette.websockets import WebSocket, WebSocketDisconnect
from typing_extensions import TypedDict
_HTTP_METHODS = frozenset(
{
"CONNECT",
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT",
"QUERY",
"TRACE",
}
)
class TelemetryConfig(TypedDict, total=False):
"""Optional settings for `FastAPI(telemetry={...})`.
Omitted settings keep their defaults. FastAPI preserves existing providers
and their exporters. Environment setup can add exporters unless `auto_configure`
is `False`. Providers supplied by the application are never shut down by FastAPI.
"""
tracer_provider: Annotated[
TracerProvider | None,
Doc("Use this provider instead of the global tracer provider."),
]
meter_provider: Annotated[
MeterProvider | None,
Doc("Use this provider instead of the global meter provider."),
]
logger_provider: Annotated[
LoggerProvider | None,
Doc("Use this provider instead of the global logger provider."),
]
tracing: Annotated[
bool,
Doc("Enable HTTP request and WebSocket connection spans. Defaults to `True`."),
]
metrics: Annotated[bool, Doc("Enable HTTP request metrics. Defaults to `True`.")]
logs: Annotated[
bool,
Doc(
"Record validation failures and unhandled exceptions, including exception messages and stack traces. Defaults to `True`."
),
]
operation_spans: Annotated[
bool,
Doc(
"Trace dependency resolution, endpoints, serialization, and background tasks. Defaults to `True`."
),
]
auto_configure: Annotated[
bool,
Doc("Add OTLP exporters from environment variables. Defaults to `True`."),
]
exclude: Annotated[
Callable[[MutableMapping[str, Any]], bool] | None,
Doc(
"A function that receives the ASGI scope. Return `True` to skip FastAPI telemetry for that request or connection."
),
]
@dataclass(kw_only=True)
class TelemetryData:
"""Local request or connection data for synchronous OpenTelemetry processors.
FastAPI populates these fields as it handles the request or connection.
Integrations should treat the data as read-only and apply their capture and
redaction settings before exporting any of it.
"""
request: Annotated[
Request | None, Doc("The request, available when its route handler starts.")
] = None
websocket: Annotated[
WebSocket | None,
Doc("The WebSocket, available when its route handler starts."),
] = None
body: Annotated[
Any, Doc("The body read by FastAPI, before parameter validation.")
] = None
values: Annotated[
dict[str, Any] | None,
Doc("The original parsed arguments, available after dependency resolution."),
] = None
errors: Annotated[
Sequence[Any] | None,
Doc("Request validation errors, including the original input values."),
] = None
@dataclass(kw_only=True)
class _RequestTelemetry:
span: Span | None
span_method: str
tracer: Tracer | None = None
logger: Logger | None = None
route: str | None = None
status_code: int | None = None
data: TelemetryData | None = field(default_factory=TelemetryData)
_mount_prefix: str = ""
_exceptions: list[BaseException] = field(default_factory=list, repr=False)
_REQUEST_TELEMETRY_KEY = otel_context.create_key("fastapi.request")
_NO_OPERATION = nullcontext()
def _get_request_telemetry(context: Context | None = None) -> _RequestTelemetry | None:
value = otel_context.get_value(_REQUEST_TELEMETRY_KEY, context)
return value if isinstance(value, _RequestTelemetry) else None
def get_telemetry_data(context: Context | None = None) -> TelemetryData | None:
"""Read local FastAPI data from an OpenTelemetry context.
Defaults to the current context. Log processors can pass `record.context`.
Read the data synchronously while FastAPI handles the request or connection.
After it finishes, this returns `None`, including for retained contexts.
This data is not automatically added to exported spans or logs.
"""
request_telemetry = _get_request_telemetry(context)
return request_telemetry.data if request_telemetry is not None else None
def _validation_failed(
exc: RequestValidationError | WebSocketRequestValidationError,
) -> None:
request_telemetry = _get_request_telemetry()
if request_telemetry is None:
return
if request_telemetry.data is not None:
if isinstance(exc, RequestValidationError):
request_telemetry.data.body = exc.body
request_telemetry.data.errors = exc.errors()
if request_telemetry.logger is not None:
attributes: dict[str, Any] = {
"fastapi.validation.error_count": len(exc.errors())
}
if request_telemetry.route is not None:
attributes["http.route"] = request_telemetry.route
request_telemetry.logger.emit(
event_name="fastapi.validation.failed",
timestamp=time_ns(),
severity_number=SeverityNumber.WARN,
severity_text="WARN",
body="Request validation failed",
attributes=attributes,
)
def _exception_type(exc: BaseException) -> str:
cls = type(exc)
return (
f"{cls.__module__}.{cls.__qualname__}"
if cls.__module__ != "builtins"
else cls.__qualname__
)
def _normal_websocket_disconnect(exc: BaseException) -> bool:
return isinstance(exc, WebSocketDisconnect) and exc.code in (1000, 1001)
def _operation(
*, name: str, function: Callable[..., Any] | None = None
) -> AbstractContextManager[None]:
request_telemetry = _get_request_telemetry()
if request_telemetry is None or request_telemetry.tracer is None:
return _NO_OPERATION
return _traced_operation(
request_telemetry=request_telemetry, name=name, function=function
)
@contextmanager
def _traced_operation(
*,
request_telemetry: _RequestTelemetry,
name: str,
function: Callable[..., Any] | None,
) -> Iterator[None]:
assert request_telemetry.tracer is not None
attributes = {}
if function is not None:
module = getattr(function, "__module__", type(function).__module__)
qualname = getattr(function, "__qualname__", type(function).__qualname__)
attributes["code.function.name"] = f"{module}.{qualname}"
with request_telemetry.tracer.start_as_current_span(
f"fastapi.{name}",
attributes=attributes,
record_exception=False,
set_status_on_exception=False,
) as span:
try:
yield
except Exception as exc:
if name == "background_task" or (
not isinstance(
exc,
(
HTTPException,
RequestValidationError,
WebSocketException,
WebSocketRequestValidationError,
),
)
and not _normal_websocket_disconnect(exc)
):
span.set_attribute("error.type", _exception_type(exc))
span.set_status(StatusCode.ERROR)
raise
def _run_sync_endpoint(
*, function: Callable[..., Any], arguments: dict[str, Any]
) -> Any:
with _operation(name="endpoint", function=function):
return function(**arguments)
def _route_selected(
*, scope: MutableMapping[str, Any], path: str | None, mount: bool = False
) -> None:
request_telemetry = scope.get("fastapi.telemetry")
if not isinstance(request_telemetry, _RequestTelemetry) or path is None:
return
# Mount's path_format ends in /{path}. The child contributes its own path.
if mount:
path = path.removesuffix("/{path}")
request_telemetry._mount_prefix += path
request_telemetry.route = request_telemetry._mount_prefix + "/{path}"
else:
request_telemetry.route = request_telemetry._mount_prefix + path
if request_telemetry.span is not None:
request_telemetry.span.set_attribute("http.route", request_telemetry.route)
request_telemetry.span.update_name(
f"{request_telemetry.span_method} {request_telemetry.route}"
)
_DEFERRED_PROVIDERS = frozenset(
{
("opentelemetry.trace", "ProxyTracerProvider"),
("opentelemetry.metrics._internal", "_ProxyMeterProvider"),
("opentelemetry._logs._internal", "ProxyLoggerProvider"),
}
)
def _unconfigured(provider: Any) -> bool:
# Python's API has no public "is configured" query. Limit this bridge to its
# known deferred providers, never SDK implementations or vendor proxies.
cls = type(provider)
return (cls.__module__, cls.__name__) in _DEFERRED_PROVIDERS
+446
View File
@@ -0,0 +1,446 @@
import os
from contextlib import nullcontext
from time import perf_counter, time_ns
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlsplit
from fastapi import __version__
from fastapi.telemetry._api import (
_HTTP_METHODS,
_REQUEST_TELEMETRY_KEY,
TelemetryConfig,
_exception_type,
_normal_websocket_disconnect,
_RequestTelemetry,
_unconfigured,
)
from opentelemetry import _logs, metrics, propagate, trace
from opentelemetry import context as otel_context
from opentelemetry._logs import SeverityNumber
from opentelemetry.metrics import Histogram, UpDownCounter
from opentelemetry.propagators.textmap import Getter
from opentelemetry.trace import SpanKind, StatusCode
from starlette.datastructures import Headers
from starlette.types import ASGIApp, Message, Receive, Scope, Send
_DURATION_BUCKETS = (
0.005,
0.01,
0.025,
0.05,
0.075,
0.1,
0.25,
0.5,
0.75,
1.0,
2.5,
5.0,
7.5,
10.0,
)
_SCHEMA_URL = "https://opentelemetry.io/schemas/1.44.0"
# https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-span
_SENSITIVE_QUERY_PARAMETERS = frozenset(
{
"X-Amz-Signature",
"X-Amz-Credential",
"X-Amz-Security-Token",
"sig",
"X-Goog-Signature",
}
)
class _HeadersGetter(Getter[Headers]):
def get(self, carrier: Headers, key: str) -> list[str] | None:
values = carrier.getlist(key)
if not values:
return None
if key.lower() == "baggage":
# The baggage propagator reads only the first value.
return [",".join(values)]
return values
def keys(self, carrier: Headers) -> list[str]:
return carrier.keys()
_HEADERS_GETTER = _HeadersGetter()
def _legacy_otel(stack: Any) -> bool:
# Test the built stack, not contrib's flag: that flag also exists when its
# stack patch failed, or when instrument_app was called after stack creation.
seen: set[int] = set()
while stack is not None and id(stack) not in seen:
seen.add(id(stack))
if (
type(stack).__module__ == "opentelemetry.instrumentation.asgi"
and type(stack).__name__ == "OpenTelemetryMiddleware"
):
return True
stack = getattr(stack, "app", None)
return False
def _exception(*, scope: Scope, exc: BaseException) -> None:
if scope["type"] == "websocket" and _normal_websocket_disconnect(exc):
return
request_telemetry = scope.get("fastapi.telemetry")
if isinstance(request_telemetry, _RequestTelemetry):
seen = request_telemetry._exceptions
if not any(previous is exc for previous in seen):
seen.append(exc)
if request_telemetry.logger is not None and isinstance(exc, Exception):
attributes = (
{"http.route": request_telemetry.route}
if request_telemetry.route
else None
)
request_telemetry.logger.emit(
exception=exc,
event_name="fastapi.websocket.exception"
if scope["type"] == "websocket"
else "http.server.request.exception",
timestamp=time_ns(),
severity_number=SeverityNumber.ERROR,
severity_text="ERROR",
body="Unhandled exception in FastAPI WebSocket connection"
if scope["type"] == "websocket"
else "Unhandled exception in FastAPI request",
attributes=attributes,
)
def _server_attributes(scope: Scope) -> dict[str, Any]:
# ASGI exposes HTTP/2 and HTTP/3 authority as Host. Proxy normalization is
# handled by the server or middleware, as it is for the request URL.
authority = Headers(scope=scope).get("host")
if authority is not None:
try:
url = urlsplit("//" + authority)
address, port = url.hostname, url.port
except ValueError:
# Malformed authority must not prevent handling the request.
return {}
if url.username is not None or url.path or url.query or url.fragment:
return {}
if port is None:
port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(
scope.get("scheme", "http")
)
else:
address, port = scope.get("server") or (None, None)
if address is None:
return {}
attributes: dict[str, Any] = {"server.address": address}
if port is not None:
attributes["server.port"] = port
return attributes
class ExceptionTelemetryMiddleware:
"""Observe exceptions before error handlers send and complete a response."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
try:
await self.app(scope, receive, send)
except Exception as exc:
_exception(scope=scope, exc=exc)
raise
class NativeTelemetry:
def __init__(self, config: TelemetryConfig) -> None:
self.config = config
known_methods = os.getenv("OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS", "").strip()
self._known_methods = (
frozenset(
method.strip() for method in known_methods.split(",") if method.strip()
)
if known_methods
else _HTTP_METHODS
)
self._provider: metrics.MeterProvider | None = None
self._duration: Histogram | None = None
self._active: UpDownCounter | None = None
def enabled(self) -> bool:
config = self.config
return bool(
(
config["tracing"]
and (
config["tracer_provider"] is not None
or not _unconfigured(trace.get_tracer_provider())
)
)
or (
config["metrics"]
and (
config["meter_provider"] is not None
or not _unconfigured(metrics.get_meter_provider())
)
)
or (
config["logs"]
and (
config["logger_provider"] is not None
or not _unconfigured(_logs.get_logger_provider())
)
)
)
def _instruments(self) -> tuple[Histogram, UpDownCounter]:
provider = self.config["meter_provider"] or metrics.get_meter_provider()
if provider is not self._provider:
meter = provider.get_meter("fastapi", __version__, schema_url=_SCHEMA_URL)
self._duration = meter.create_histogram(
"http.server.request.duration",
unit="s",
description="Duration of HTTP server requests.",
explicit_bucket_boundaries_advisory=_DURATION_BUCKETS,
)
self._active = meter.create_up_down_counter(
"http.server.active_requests",
unit="{request}",
description="Number of active HTTP server requests.",
)
self._provider = provider
assert self._duration is not None and self._active is not None
return self._duration, self._active
async def __call__(
self,
*,
app: ASGIApp,
scope: Scope,
receive: Receive,
send: Send,
legacy_otel: bool = False,
) -> None:
config = self.config
if config["exclude"] is not None and config["exclude"](scope):
# Keep mounted FastAPI apps from observing this excluded request.
scope["fastapi.telemetry"] = None
try:
await app(scope, receive, send)
finally:
scope.pop("fastapi.telemetry", None)
return
tracing = config["tracing"] and not legacy_otel
logging = config["logs"] and not legacy_otel
if logging:
logger_provider = config["logger_provider"] or _logs.get_logger_provider()
logging = not (
(config["logger_provider"] is None and _unconfigured(logger_provider))
or isinstance(logger_provider, _logs.NoOpLoggerProvider)
)
is_websocket = scope["type"] == "websocket"
metering = config["metrics"] and not legacy_otel and not is_websocket
if tracing:
provider = config["tracer_provider"] or trace.get_tracer_provider()
tracing = not (
(config["tracer_provider"] is None and _unconfigured(provider))
or isinstance(provider, trace.NoOpTracerProvider)
)
if metering:
meter_provider = config["meter_provider"] or metrics.get_meter_provider()
metering = not (
(config["meter_provider"] is None and _unconfigured(meter_provider))
or isinstance(meter_provider, metrics.NoOpMeterProvider)
)
if not tracing and not metering and not logging:
await app(scope, receive, send)
return
original_method = scope.get("method", "")
method = original_method if original_method in self._known_methods else "_OTHER"
span_method = "WS" if is_websocket else "HTTP" if method == "_OTHER" else method
attributes: dict[str, Any] = {
"url.scheme": scope.get("scheme", "ws" if is_websocket else "http"),
}
if is_websocket:
attributes["network.protocol.name"] = "websocket"
else:
attributes["http.request.method"] = method
if scope.get("http_version"):
attributes["network.protocol.version"] = scope["http_version"]
duration, active = self._instruments() if metering else (None, None)
active_attributes = {
k: v for k, v in attributes.items() if k != "network.protocol.version"
}
if active is not None:
active.add(1, active_attributes)
started = perf_counter()
span = None
tracer = None
parent_token = None
if tracing:
parent = propagate.extract(Headers(scope=scope), getter=_HEADERS_GETTER)
parent_token = otel_context.attach(parent)
tracer = trace.get_tracer(
"fastapi",
__version__,
config["tracer_provider"],
schema_url=_SCHEMA_URL,
)
span_attributes = {**attributes, **_server_attributes(scope)}
if not is_websocket:
span_attributes["url.path"] = scope["path"]
if scope.get("query_string"):
span_attributes["url.query"] = urlencode(
[
(
key,
"REDACTED"
if key in _SENSITIVE_QUERY_PARAMETERS
else value,
)
for key, value in parse_qsl(
scope["query_string"].decode("latin-1"),
keep_blank_values=True,
)
]
)
if original_method != method:
span_attributes["http.request.method_original"] = original_method
span = tracer.start_span(
span_method,
context=parent,
kind=SpanKind.SERVER,
attributes=span_attributes,
)
request_telemetry = _RequestTelemetry(
span=span,
span_method=span_method,
tracer=tracer if config["operation_spans"] else None,
logger=_logs.get_logger(
"fastapi",
__version__,
config["logger_provider"],
schema_url=_SCHEMA_URL,
)
if logging
else None,
_mount_prefix=scope.get("app_root_path", scope.get("root_path", "")).rstrip(
"/"
),
)
scope["fastapi.telemetry"] = request_telemetry
token = otel_context.attach(
otel_context.set_value(_REQUEST_TELEMETRY_KEY, request_telemetry)
)
finished = False
trailers = False
disconnected = False
def finish(error: BaseException | None = None) -> None:
nonlocal finished
if finished:
return
finished = True
if request_telemetry.route is not None:
attributes["http.route"] = request_telemetry.route
if request_telemetry.status_code is not None:
attributes["http.response.status_code"] = request_telemetry.status_code
failed = (
error is not None
or not is_websocket
and (
request_telemetry.status_code is None
or request_telemetry.status_code >= 500
)
)
if failed:
errors = request_telemetry._exceptions
failure = error or (errors[-1] if errors else None)
attributes["error.type"] = (
_exception_type(failure)
if failure
else str(request_telemetry.status_code or "incomplete_response")
)
if span is not None:
span.set_attributes(attributes)
if failed:
span.set_status(StatusCode.ERROR)
# Record while the span is current, so exemplars can correlate it.
if duration is not None:
duration.record(max(0, perf_counter() - started), attributes)
if active is not None:
active.add(-1, active_attributes)
if span is not None:
span.end()
async def wrapped_send(message: Message) -> None:
nonlocal trailers
if message["type"] == "http.response.start":
request_telemetry.status_code = message["status"]
trailers = message.get("trailers", False)
await send(message)
if (
(
message["type"] == "http.response.body"
and not message.get("more_body", False)
and not trailers
)
or (
message["type"] == "http.response.trailers"
and not message.get("more_trailers", False)
)
or message["type"] == "http.response.pathsend"
):
finish()
async def wrapped_receive() -> Message:
nonlocal disconnected
message = await receive()
if message["type"] == "http.disconnect":
disconnected = True
return message
context = (
trace.use_span(
span,
end_on_exit=False,
record_exception=False,
set_status_on_exception=False,
)
if span is not None
else nullcontext()
)
try:
with context:
try:
await app(
scope,
receive if is_websocket else wrapped_receive,
send if is_websocket else wrapped_send,
)
except BaseException as exc:
_exception(scope=scope, exc=exc)
finish(
None
if is_websocket and _normal_websocket_disconnect(exc)
else exc
)
raise
finally:
if not finished:
finish(
None
if is_websocket
else ConnectionError("Client disconnected")
if disconnected
else RuntimeError("Incomplete ASGI response")
)
finally:
request_telemetry.data = None
request_telemetry._exceptions.clear()
otel_context.detach(token)
scope.pop("fastapi.telemetry", None)
if parent_token is not None:
otel_context.detach(parent_token)
+238
View File
@@ -0,0 +1,238 @@
import atexit
import os
import threading
from collections.abc import Callable
from typing import Any, cast
from urllib.parse import urlsplit
from anyio.to_thread import run_sync
from fastapi.exceptions import FastAPIError
from fastapi.logger import logger
from fastapi.telemetry._api import TelemetryConfig, _unconfigured
from opentelemetry import _logs, metrics, trace
from starlette.types import ASGIApp, Message, Receive, Scope, Send
_lock = threading.RLock()
_owned: list[Any] = []
_configured: list[tuple[str, Any]] = []
def _flush() -> None:
for component in tuple(_owned):
try:
component.force_flush()
except Exception:
logger.exception("FastAPI telemetry cleanup failed")
def _shutdown() -> None:
with _lock:
owned = tuple(_owned)
_owned.clear()
for component in owned:
try:
component.shutdown()
except Exception:
logger.exception("FastAPI telemetry cleanup failed")
atexit.register(_shutdown)
def _export_endpoint(signal: str) -> str | None:
exporter = (os.getenv(f"OTEL_{signal}_EXPORTER") or "otlp").strip().lower()
endpoint = os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_ENDPOINT")
if not endpoint:
endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
if endpoint:
endpoint = f"{endpoint.removesuffix('/')}/v1/{signal.lower()}"
if not endpoint or exporter == "none":
return None
if exporter != "otlp":
raise FastAPIError(
f"FastAPI automatic telemetry supports OTEL_{signal}_EXPORTER=otlp or none. "
"Configure other exporters explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
)
protocol = (
os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_PROTOCOL")
or os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
or "http/protobuf"
)
if protocol != "http/protobuf":
raise FastAPIError(
"FastAPI automatic telemetry requires the OTLP http/protobuf protocol. Configure other transports explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
)
parsed = urlsplit(endpoint)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise FastAPIError(
f"Invalid OTLP {signal.lower()} endpoint. Use an absolute HTTP or HTTPS URL."
)
return endpoint
def _registration_provider(provider: Any) -> Any:
# Older Logfire meter wrappers do not expose add_metric_reader(). Attach
# the reader to their SDK provider. Measurements still use the original wrapper.
if (
type(provider).__module__ == "logfire._internal.metrics"
and type(provider).__name__ == "ProxyMeterProvider"
and not hasattr(provider, "add_metric_reader")
):
return provider.provider
return provider
def _configure_from_environment(config: TelemetryConfig) -> None:
"""Add OTLP exporters from the environment to the selected providers.
Called automatically before ASGI lifespan startup. Existing providers and
their exporters are preserved. FastAPI registers its own export components once
per provider. It does not inspect or deduplicate other components' exporters.
Pass `telemetry={"auto_configure": False}` to `FastAPI()` when another component
manages environment export.
"""
if (
not config["auto_configure"]
or os.getenv("OTEL_SDK_DISABLED", "").lower() == "true"
):
return
with _lock:
signals = [
("TRACES", config["tracing"], config["tracer_provider"]),
("METRICS", config["metrics"], config["meter_provider"]),
("LOGS", config["logs"], config["logger_provider"]),
]
requested = [
(signal, provider, endpoint)
for signal, enabled, provider in signals
if enabled and (endpoint := _export_endpoint(signal)) is not None
]
if not requested:
return
try:
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter,
)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
except ImportError as exc:
raise FastAPIError(
"Automatic OpenTelemetry export requires fastapi[opentelemetry] or fastapi[standard]. Install the extra, configure providers yourself, or pass telemetry={'auto_configure': False} to FastAPI()."
) from exc
# The SDK's registration methods also allow vendor wrappers to forward
# additions. Keep a record of our own registrations, without examining
# another component's exporters or their destinations.
providers = {
"TRACES": ("tracer", trace.get_tracer_provider),
"METRICS": ("meter", metrics.get_meter_provider),
"LOGS": ("logger", _logs.get_logger_provider),
}
def create_component(*, signal: str, endpoint: str) -> Any:
if signal == "TRACES":
return BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
if signal == "METRICS":
return PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=endpoint)
)
return BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint))
for signal, explicit, endpoint in requested:
name, get_provider = providers[signal]
provider = _registration_provider(
explicit if explicit is not None else get_provider()
)
if any(s == signal and p is provider for s, p in _configured):
continue
if explicit is None and _unconfigured(provider):
component = create_component(signal=signal, endpoint=endpoint)
if signal == "TRACES":
provider = TracerProvider(shutdown_on_exit=False)
provider.add_span_processor(component)
trace.set_tracer_provider(provider)
elif signal == "METRICS":
provider = MeterProvider(
metric_readers=[component], shutdown_on_exit=False
)
metrics.set_meter_provider(provider)
else:
provider = LoggerProvider(shutdown_on_exit=False)
provider.add_log_record_processor(component)
_logs.set_logger_provider(provider)
current = get_provider()
if current is provider:
_configured.append((signal, provider))
_owned.append(provider)
continue
# Another component configured the global provider concurrently.
# Close the unused pipeline and attach a new component to theirs.
provider.shutdown()
provider = _registration_provider(current)
# Vendor wrappers can expose the SDK registration methods without
# inheriting its provider classes. Check each method against its SDK
# class statically and its availability on the provider at runtime.
register: Callable[[Any], None] | None
try:
if signal == "TRACES":
register = cast(TracerProvider, provider).add_span_processor
elif signal == "METRICS":
register = cast(MeterProvider, provider).add_metric_reader
else:
register = cast(LoggerProvider, provider).add_log_record_processor
except AttributeError:
register = None
if not callable(register):
raise FastAPIError(
f"The configured OpenTelemetry {name} provider does not support "
"adding an OTLP exporter. Configure environment export through "
"that provider and pass telemetry={'auto_configure': False} to FastAPI()."
)
component = create_component(signal=signal, endpoint=endpoint)
try:
register(component)
except Exception:
component.shutdown()
raise
_configured.append((signal, provider))
_owned.append(component)
async def lifespan(
*, config: TelemetryConfig, app: ASGIApp, scope: Scope, receive: Receive, send: Send
) -> None:
async def wrapped_receive() -> Message:
message = await receive()
if message["type"] == "lifespan.startup":
try:
_configure_from_environment(config)
except Exception as exc:
# Optional telemetry setup must not prevent application startup.
logger.warning(
"FastAPI automatic telemetry configuration failed: %s", exc
)
return message
async def wrapped_send(message: Message) -> None:
if (
message["type"]
in {
"lifespan.shutdown.complete",
"lifespan.shutdown.failed",
"lifespan.startup.failed",
}
and _owned
):
await run_sync(_flush)
await send(message)
await app(scope, wrapped_receive, wrapped_send)
+19 -1
View File
@@ -47,6 +47,7 @@ dependencies = [
"typing-extensions>=4.8.0",
"typing-inspection>=0.4.2",
"annotated-doc>=0.0.2",
"opentelemetry-api>=1.44.0",
]
[project.urls]
@@ -57,7 +58,13 @@ Issues = "https://github.com/fastapi/fastapi/issues"
Changelog = "https://fastapi.tiangolo.com/release-notes/"
[project.optional-dependencies]
opentelemetry = [
"opentelemetry-sdk>=1.44.0",
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
]
standard = [
"opentelemetry-sdk>=1.44.0",
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
"fastapi-cli[standard] >=0.0.32",
"fastar >= 0.9.0",
# For the test client
@@ -77,6 +84,8 @@ standard = [
]
standard-no-fastapi-cloud-cli = [
"opentelemetry-sdk>=1.44.0",
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
"fastapi-cli[standard-no-fastapi-cloud-cli] >=0.0.32",
# For the test client
"httpx >=0.23.0,<1.0.0",
@@ -95,6 +104,8 @@ standard-no-fastapi-cloud-cli = [
]
all = [
"opentelemetry-sdk>=1.44.0",
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
"fastapi-cli[standard] >=0.0.32",
# # For the test client
"httpx >=0.23.0,<1.0.0",
@@ -147,7 +158,7 @@ docs = [
docs-tests = [
"httpx >=0.23.0,<1.0.0",
"httpx2>=2.0.0",
"ruff >=0.14.14,<0.16.0",
"ruff>=0.14.14,<0.17.0",
]
github-actions = [
"httpx >=0.27.0,<1.0.0",
@@ -158,6 +169,12 @@ github-actions = [
"smokeshow >=0.5.0",
]
tests = [
"opentelemetry-instrumentation-fastapi>=0.65b0",
"logfire>=5.1.0",
"sentry-sdk>=2.70.0",
"opentelemetry-sdk>=1.44.0",
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
{ include-group = "docs-tests" },
"anyio[trio] >=3.2.1,<5.0.0",
"coverage[toml] >=7.13,<8.0",
@@ -233,6 +250,7 @@ filterwarnings = [
timeout = "20"
[tool.coverage.run]
patch = ["subprocess", "_exit"]
parallel = true
data_file = "coverage/.coverage"
source = [
+4 -1
View File
@@ -1,4 +1,5 @@
import json
import os
import secrets
from collections.abc import Iterable
from functools import lru_cache
@@ -37,6 +38,8 @@ general_prompt = general_prompt_path.read_text(encoding="utf-8")
app = typer.Typer()
repository_path = Path(__file__).absolute().parent.parent
os.environ["PYDANTIC_AI_NO_BANNER"] = "1"
@lru_cache
def get_langs() -> dict[str, str]:
@@ -137,7 +140,7 @@ def translate_page(
print(f"Found existing translation: {out_path}")
old_translation = out_path.read_text(encoding="utf-8")
print(f"Translating {en_path} to {language} ({language_name})")
agent = Agent("openai-chat:gpt-5.5")
agent = Agent("openai-chat:gpt-6-astra")
MAX_ATTEMPTS = 3
additional_instructions = ""
+2 -5
View File
@@ -578,11 +578,8 @@ async def test_frontend_dependency_restores_existing_dependency_stacks(
messages = []
async def receive():
return { # pragma: no cover
"type": "http.request",
"body": b"",
"more_body": False,
}
# Keep the connection open until the response finishes.
await anyio.sleep_forever()
async def send(message):
messages.append(message)
+164
View File
@@ -0,0 +1,164 @@
from functools import wraps
import pytest
from fastapi import APIRouter, Depends, FastAPI, routing
from fastapi.responses import PlainTextResponse
from fastapi.testclient import TestClient
@pytest.mark.parametrize("include_router", [False, True], ids=["direct", "included"])
def test_endpoint_wrapper_does_not_accumulate_across_requests(
monkeypatch, include_router
):
original_get_request_handler = routing.get_request_handler
wrapper_calls = 0
def get_request_handler_with_endpoint_wrapper(*args, **kwargs):
# Older Sentry SDKs wrap dependant.call each time a handler is built.
# Accumulating these wrappers eventually exhausts the recursion limit.
dependant = kwargs["dependant"]
original_call = dependant.call
@wraps(original_call)
def wrapped_endpoint(*args, **kwargs):
nonlocal wrapper_calls
wrapper_calls += 1
return original_call(*args, **kwargs)
dependant.call = wrapped_endpoint
return original_get_request_handler(*args, **kwargs)
monkeypatch.setattr(
routing, "get_request_handler", get_request_handler_with_endpoint_wrapper
)
app = FastAPI()
router = APIRouter() if include_router else app.router
@router.get("/items/{item_id}")
def read_item(item_id: str):
return {"item_id": item_id}
if include_router:
app.include_router(router)
calls_per_request = []
with TestClient(app) as client:
for item_id in ("first", "second", "third"):
wrapper_calls = 0
response = client.get(f"/items/{item_id}")
assert response.status_code == 200
assert response.json() == {"item_id": item_id}
calls_per_request.append(wrapper_calls)
assert calls_per_request == [1, 1, 1]
def test_custom_handlers_are_cached_separately_for_nested_inclusions():
built_handlers = []
class CustomRoute(routing.APIRoute):
def get_route_handler(self):
handler = super().get_route_handler()
built_handlers.append(handler)
handler_id = str(len(built_handlers))
async def custom_handler(request):
response = await handler(request)
response.headers["x-handler-id"] = handler_id
return response
return custom_handler
router = APIRouter(route_class=CustomRoute)
@router.get("/{item_id}")
def read_item(item_id: str):
return item_id
parent = APIRouter()
parent.include_router(router, prefix="/json")
parent.include_router(
router, prefix="/text", default_response_class=PlainTextResponse
)
app = FastAPI()
app.include_router(parent, prefix="/api")
with TestClient(app) as client:
first_json = client.get("/api/json/first")
first_text = client.get("/api/text/first")
json_handler_id = first_json.headers["x-handler-id"]
text_handler_id = first_text.headers["x-handler-id"]
assert json_handler_id != text_handler_id
built_count = len(built_handlers)
for item_id in ("second", "third"):
json_response = client.get(f"/api/json/{item_id}")
assert json_response.status_code == 200
assert json_response.json() == item_id
assert json_response.headers["x-handler-id"] == json_handler_id
text_response = client.get(f"/api/text/{item_id}")
assert text_response.status_code == 200
assert text_response.text == item_id
assert text_response.headers["x-handler-id"] == text_handler_id
assert len(built_handlers) == built_count
def test_cached_handler_uses_live_dependency_overrides_and_route_additions():
router = APIRouter()
def dependency():
return "original"
@router.get("/items")
def read_items(value: str = Depends(dependency)):
return value
app = FastAPI()
app.include_router(router, prefix="/api")
with TestClient(app) as client:
assert client.get("/api/items").json() == "original"
app.dependency_overrides[dependency] = lambda: "overridden"
assert client.get("/api/items").json() == "overridden"
@router.get("/later")
def read_later(value: str = Depends(dependency)):
return value
assert client.get("/api/items").json() == "overridden"
assert client.get("/api/later").json() == "overridden"
app.dependency_overrides.clear()
assert client.get("/api/items").json() == "original"
assert client.get("/api/later").json() == "original"
def test_failed_handler_construction_restores_context_and_can_retry():
fail = False
class CustomRoute(routing.APIRoute):
def get_route_handler(self):
if fail:
raise RuntimeError("handler construction failed")
return super().get_route_handler()
router = APIRouter(route_class=CustomRoute)
@router.get("/items")
def read_items():
return ["item"]
app = FastAPI()
app.include_router(router, prefix="/api")
fail = True
assert routing._effective_route_context_var.get() is None
with pytest.raises(RuntimeError, match="handler construction failed"):
app.openapi()
assert routing._effective_route_context_var.get() is None
fail = False
assert "/api/items" in app.openapi()["paths"]
assert routing._effective_route_context_var.get() is None
with TestClient(app) as client:
assert client.get("/api/items").json() == ["item"]
View File
Whitespace-only changes.
+36
View File
@@ -0,0 +1,36 @@
import threading
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from socketserver import TCPServer
class _CollectorServer(ThreadingHTTPServer):
def server_bind(self):
TCPServer.server_bind(self)
self.server_name = str(self.server_address[0])
self.server_port = self.server_address[1]
@contextmanager
def otlp_collector():
received = []
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
received.append((self.path, body, self.headers))
self.send_response(200)
self.send_header("Content-Type", "application/x-protobuf")
self.end_headers()
def log_message(self, format, *args):
pass
with _CollectorServer(("127.0.0.1", 0), Handler) as server:
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
try:
yield f"http://127.0.0.1:{server.server_port}", received
finally:
server.shutdown()
worker.join()
+43
View File
@@ -0,0 +1,43 @@
import json
import os
import subprocess
import sys
from functools import wraps
from importlib import import_module
from inspect import unwrap
def run_in_subprocess(function):
"""Run a test in a fresh interpreter to isolate SDK globals and monkeypatches."""
@wraps(function)
def wrapper(**kwargs):
env = {
name: value
for name, value in os.environ.items()
if not name.startswith(("OTEL_", "LOGFIRE_", "SENTRY_"))
}
env["PYDANTIC_DISABLE_PLUGINS"] = "__all__"
result = subprocess.run(
[
sys.executable,
"-m",
"tests.test_telemetry._subprocess",
function.__module__,
function.__name__,
json.dumps(kwargs),
],
env=env,
capture_output=True,
text=True,
timeout=15,
)
assert result.returncode == 0, result.stdout + result.stderr
return wrapper
if __name__ == "__main__":
module_name, function_name, arguments = sys.argv[1:]
function = getattr(import_module(module_name), function_name)
unwrap(function)(**json.loads(arguments))
+69
View File
@@ -0,0 +1,69 @@
import os
import pytest
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
@pytest.fixture(autouse=True)
def clean_environment(monkeypatch):
remove_export_environment(monkeypatch)
def remove_export_environment(monkeypatch):
for name in os.environ:
if name.startswith(("OTEL_", "LOGFIRE_", "SENTRY_")):
monkeypatch.delenv(name)
@pytest.fixture
def telemetry():
exporter = InMemorySpanExporter()
tracer = TracerProvider(shutdown_on_exit=False)
tracer.add_span_processor(SimpleSpanProcessor(exporter))
reader = InMemoryMetricReader()
meter = MeterProvider(metric_readers=[reader], shutdown_on_exit=False)
yield {"tracer_provider": tracer, "meter_provider": meter}, exporter, reader
tracer.shutdown()
meter.shutdown()
@pytest.fixture
def logs(telemetry):
config, _, _ = telemetry
exporter = InMemoryLogRecordExporter()
provider = LoggerProvider(shutdown_on_exit=False)
provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
config["logger_provider"] = provider
yield exporter
provider.shutdown()
def metric_points(*, reader, name="http.server.request.duration"):
data = reader.get_metrics_data()
if data is None:
return []
return [
point
for resource in data.resource_metrics
for scope in resource.scope_metrics
for metric in scope.metrics
if metric.name == name
for point in metric.data.data_points
]
def server_spans(exporter):
from opentelemetry.trace import SpanKind
return [
span for span in exporter.get_finished_spans() if span.kind == SpanKind.SERVER
]
+276
View File
@@ -0,0 +1,276 @@
import threading
from functools import partial
import anyio
import pytest
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.telemetry import get_telemetry_data
from fastapi.testclient import TestClient
from opentelemetry import trace
from opentelemetry.sdk.trace import Span
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
from opentelemetry.trace import SpanKind, StatusCode
from starlette.background import BackgroundTask
from ._subprocess import run_in_subprocess
from .conftest import metric_points, server_spans
@pytest.mark.parametrize("backend", ["asyncio", "trio"])
@pytest.mark.parametrize("sync", [False, True])
def test_background_task_spans(telemetry, backend, sync):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
executed = []
endpoint_thread = []
def execute(number, *, value):
span = trace.get_current_span()
assert isinstance(span, Span)
assert span.is_recording()
assert span.name == "fastapi.background_task"
data = get_telemetry_data()
assert data is not None
assert data.request is not None
assert data.request.url.path == "/items/42"
assert len(server_spans(exporter)) == 1
executed.append((number, value, threading.get_ident(), span.get_span_context()))
async def async_task(number, *, value):
execute(number, value=value)
task = execute if sync else async_task
async def dependency(background: BackgroundTasks):
background.add_task(task, 1, value="private dependency value")
@app.get("/items/{item_id}", dependencies=[Depends(dependency)])
async def endpoint(background: BackgroundTasks, item_id: int):
endpoint_thread.append(threading.get_ident())
background.add_task(task, 2, value="private endpoint value")
return {"item_id": item_id}
assert TestClient(app, backend=backend).get("/items/42").json() == {"item_id": 42}
assert [(number, value) for number, value, _, _ in executed] == [
(1, "private dependency value"),
(2, "private endpoint value"),
]
spans = exporter.get_finished_spans()
(server,) = server_spans(exporter)
tasks = [span for span in spans if span.name == "fastapi.background_task"]
assert len(tasks) == 2
for span, (_, _, thread_id, context) in zip(tasks, executed, strict=True):
assert span.kind == SpanKind.INTERNAL
assert span.parent.span_id == server.context.span_id
assert context.trace_id == server.context.trace_id
assert context.span_id == span.context.span_id
assert span.start_time >= server.end_time
assert span.status.status_code == StatusCode.UNSET
assert span.attributes == {
"code.function.name": f"{task.__module__}.{task.__qualname__}"
}
assert (thread_id != endpoint_thread[0]) == sync
assert metric_points(reader=reader)[0].count == 1
assert get_telemetry_data() is None
def test_background_task_objects(telemetry):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
executed = []
def record(value):
span = trace.get_current_span()
assert isinstance(span, Span)
executed.append((value, span.name))
class AsyncTask:
async def __call__(self, *, value):
record(value)
class CustomTask(BackgroundTask):
async def __call__(self):
record("before")
await super().__call__()
record("after")
original = CustomTask(record, "custom")
background = BackgroundTasks([original])
background.add_task(partial(record, "partial"))
background.add_task(AsyncTask(), value="async callable")
@app.get("/")
async def endpoint():
return JSONResponse("ok", background=background)
assert TestClient(app).get("/").json() == "ok"
assert background.tasks[0] is original
assert executed == [
(value, "fastapi.background_task")
for value in ("before", "custom", "after", "partial", "async callable")
]
assert (
len(
[
s
for s in exporter.get_finished_spans()
if s.name == "fastapi.background_task"
]
)
== 3
)
@pytest.mark.parametrize("sync", [False, True])
@pytest.mark.parametrize("http_error", [False, True])
def test_background_task_failure(telemetry, logs, sync, http_error):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
error = HTTPException(503) if http_error else ValueError("background failure")
executed = []
def fail():
executed.append("first")
raise error
async def async_fail():
fail()
@app.get("/")
async def endpoint(background: BackgroundTasks):
background.add_task(fail if sync else async_fail)
background.add_task(executed.append, "second")
return "ok"
assert TestClient(app, raise_server_exceptions=False).get("/").status_code == 200
assert executed == ["first"]
(task,) = [
s for s in exporter.get_finished_spans() if s.name == "fastapi.background_task"
]
assert task.status.status_code == StatusCode.ERROR
assert task.attributes["error.type"] == (
"fastapi.exceptions.HTTPException" if http_error else "ValueError"
)
assert not task.events
(server,) = server_spans(exporter)
assert server.attributes["http.response.status_code"] == 200
assert server.status.status_code == StatusCode.UNSET
(record,) = logs.get_finished_logs()
assert (
record.log_record.trace_id == task.context.trace_id == server.context.trace_id
)
assert "raise error" in record.log_record.attributes["exception.stacktrace"]
assert record.log_record.exception is not None
if http_error:
assert record.log_record.exception.__cause__ is error
else:
assert record.log_record.exception is error
@pytest.mark.parametrize(
"mode", ["operations_disabled", "tracing_disabled", "excluded", "unsampled"]
)
def test_background_task_settings(telemetry, mode):
config, exporter, _ = telemetry
if mode == "operations_disabled":
config["operation_spans"] = False
elif mode == "tracing_disabled":
config["tracing"] = False
elif mode == "excluded":
config["exclude"] = lambda scope: True
else:
config["tracer_provider"].sampler = ALWAYS_OFF
app = FastAPI(telemetry=config)
executed = []
@app.get("/")
async def endpoint(background: BackgroundTasks):
background.add_task(executed.append, "done")
return "ok"
assert TestClient(app).get("/").json() == "ok"
assert executed == ["done"]
assert not any(
s.name == "fastapi.background_task" for s in exporter.get_finished_spans()
)
def test_background_tasks_outside_request():
executed = []
background = BackgroundTasks()
background.add_task(executed.append, "done")
anyio.run(background)
assert executed == ["done"]
assert get_telemetry_data() is None
@pytest.mark.parametrize("integration", ["contrib", "logfire", "native_logfire"])
@run_in_subprocess
def test_background_task_integrations(integration):
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
native = InMemorySpanExporter()
legacy = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(native))
app = FastAPI(
telemetry={
"tracer_provider": None if integration == "native_logfire" else provider
}
)
executed = []
def sync_task():
executed.append(trace.get_current_span().get_span_context())
async def async_task():
executed.append(trace.get_current_span().get_span_context())
@app.get("/")
async def endpoint(background: BackgroundTasks):
background.add_task(sync_task)
background.add_task(async_task)
return "ok"
if integration == "contrib":
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
legacy_provider = TracerProvider()
legacy_provider.add_span_processor(SimpleSpanProcessor(legacy))
FastAPIInstrumentor.instrument_app(app, tracer_provider=legacy_provider)
else:
import logfire
logfire.configure(
send_to_logfire=False,
console=False,
metrics=False,
additional_span_processors=[SimpleSpanProcessor(legacy)],
)
if integration == "logfire":
logfire.instrument_fastapi(app)
assert TestClient(app).get("/").json() == "ok"
assert not native.get_finished_spans()
tasks = [
span
for span in legacy.get_finished_spans()
if (
span.name.startswith("BackgroundTask ")
or span.name == "fastapi.background_task"
)
and not (
span.attributes
and span.attributes.get("logfire.span_type") == "pending_span"
)
]
assert [span.name for span in tasks] == (
["fastapi.background_task", "fastapi.background_task"]
if integration == "native_logfire"
else ["BackgroundTask sync_task", "BackgroundTask async_task"]
)
assert [span.context for span in tasks] == executed
+286
View File
@@ -0,0 +1,286 @@
import gc
import weakref
import anyio
import pytest
from fastapi import Depends, FastAPI, HTTPException, Request, WebSocket
from fastapi.telemetry import get_telemetry_data
from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient
from opentelemetry import context
from opentelemetry._logs import SeverityNumber
from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from pydantic import BaseModel
from starlette.websockets import WebSocketDisconnect
from ._otlp import otlp_collector
@pytest.mark.anyio
async def test_data_is_local_to_each_request_and_cleared_afterwards(telemetry):
config, _, _ = telemetry
child = FastAPI(telemetry=config)
app = FastAPI(telemetry=config)
app.mount("/child", child)
saved_contexts = []
saved_data = []
entered = 0
ready = anyio.Event()
service = object()
class Item(BaseModel):
name: str
def dependency(request: Request):
data = get_telemetry_data()
assert data is not None
assert data.request is request
assert data.values is None
assert data.body == {"name": request.path_params["name"]}
return service
@child.post("/{name}")
async def endpoint(
*, name: str, item: Item, request: Request, value=Depends(dependency)
):
nonlocal entered
data = get_telemetry_data()
assert data is not None
assert data.request is request
assert data.values is not None
assert data.values["item"] is item
assert data.values["value"] is service
entered += 1
if entered == 2:
ready.set()
await ready.wait()
assert get_telemetry_data() is data
assert data.values["name"] == name
assert data.errors == []
saved_contexts.append(context.get_current())
saved_data.append(data)
return name
assert get_telemetry_data() is None
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
results = {}
async def send(name):
results[name] = await client.post(f"/child/{name}", json={"name": name})
async with anyio.create_task_group() as tasks:
tasks.start_soon(send, "first")
tasks.start_soon(send, "second")
assert {name: response.json() for name, response in results.items()} == {
"first": "first",
"second": "second",
}
assert saved_data[0] is not saved_data[1]
assert all(get_telemetry_data(ctx) is None for ctx in saved_contexts)
assert get_telemetry_data() is None
def test_retained_context_does_not_keep_failed_request_alive(telemetry):
config, _, _ = telemetry
app = FastAPI(telemetry={**config, "tracing": False, "logs": False})
saved_contexts = []
request_refs = []
@app.post("/")
async def endpoint(request: Request):
await request.body()
saved_contexts.append(context.get_current())
request_refs.append(weakref.ref(request))
raise ValueError("endpoint failed")
with TestClient(app, raise_server_exceptions=False) as client:
assert client.post("/", json={"name": "request data"}).status_code == 500
gc.collect()
assert get_telemetry_data(saved_contexts[0]) is None
assert request_refs[0]() is None
@pytest.mark.parametrize("invalid_json", [False, True])
@pytest.mark.parametrize("logging", [False, True])
def test_validation_data_without_tracing(invalid_json, logging):
exporter = InMemoryLogRecordExporter()
provider = LoggerProvider(shutdown_on_exit=False)
observed = []
class Validation(LogRecordProcessor):
def on_emit(self, log_record):
record = log_record.log_record
data = get_telemetry_data(record.context)
assert data is not None
assert data.request is not None
assert data.request.url.path == "/items"
assert data.errors is not None
assert data.errors[0]["type"] == (
"json_invalid" if invalid_json else "int_parsing"
)
assert data.body == ("{" if invalid_json else {"amount": "private-input"})
observed.append(record.context)
def shutdown(self):
pass
def force_flush(self, timeout_millis=30000):
return True
provider.add_log_record_processor(Validation())
provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
app = FastAPI(
telemetry={"logger_provider": provider, "tracing": False, "logs": logging}
)
@app.post("/items")
def endpoint(item: dict[str, int]):
return item # pragma: no cover
try:
with TestClient(app) as client:
response = (
client.post(
"/items", content="{", headers={"content-type": "application/json"}
)
if invalid_json
else client.post("/items", json={"amount": "private-input"})
)
assert provider.force_flush()
assert response.status_code == 422
if not logging:
assert not exporter.get_finished_logs()
assert observed == []
return
(data,) = exporter.get_finished_logs()
record = data.log_record
assert record.severity_number == SeverityNumber.WARN
assert record.event_name == "fastapi.validation.failed"
assert record.exception is None
assert record.attributes == {
"http.route": "/items",
"fastapi.validation.error_count": 1,
}
assert record.body == "Request validation failed"
assert len(observed) == 1
assert get_telemetry_data(observed[0]) is None
assert get_telemetry_data(record.context) is None
finally:
provider.shutdown()
def test_request_objects_are_not_exported():
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
ExportLogsServiceRequest,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
with otlp_collector() as (base, received):
logger = LoggerProvider(shutdown_on_exit=False)
logger.add_log_record_processor(
SimpleLogRecordProcessor(OTLPLogExporter(endpoint=base + "/v1/logs"))
)
tracer = TracerProvider(shutdown_on_exit=False)
tracer.add_span_processor(
SimpleSpanProcessor(OTLPSpanExporter(endpoint=base + "/v1/traces"))
)
app = FastAPI(telemetry={"tracer_provider": tracer, "logger_provider": logger})
service = object()
def dependency():
return service
@app.post("/items")
def endpoint(*, item: dict[str, str], count: int, value=Depends(dependency)):
data = get_telemetry_data()
assert data is not None
assert data.values is not None
assert data.values["value"] is service
raise ValueError("endpoint failed")
@app.get("/handled")
def handled():
raise HTTPException(503, "private-handled-detail")
@app.websocket("/ws")
async def websocket_endpoint(*, websocket: WebSocket, count: int):
data = get_telemetry_data()
assert data is not None
assert data.websocket is websocket
assert data.body is None
await websocket.accept()
await websocket.send_text(await websocket.receive_text())
await websocket.close()
try:
with TestClient(app, raise_server_exceptions=False) as client:
with client.websocket_connect(
"/ws?count=1&query=websocket-search",
headers={
"authorization": "Bearer authorization-secret",
"cookie": "session=session-secret",
},
) as websocket:
websocket.send_text("private-websocket-message")
assert websocket.receive_text() == "private-websocket-message"
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect("/ws?count=invalid-websocket-count"):
pass # pragma: no cover
assert client.get("/handled").status_code == 503
for count, status in [("1", 500), ("invalid-count", 422)]:
response = client.post(
"/items",
params={"count": count, "query": "search-term"},
headers={
"authorization": "Bearer authorization-secret",
"cookie": "session=session-secret",
},
json={"name": "private-body"},
)
assert response.status_code == status
assert {path for path, _, _ in received} == {"/v1/logs", "/v1/traces"}
for path, payload, _ in received:
if path == "/v1/logs":
assert b"invalid-count" not in payload
assert b"search-term" not in payload
for uncaptured_value in [
b"websocket-search",
b"invalid-websocket-count",
b"authorization-secret",
b"session-secret",
b"private-body",
b"private-handled-detail",
b"private-websocket-message",
]:
assert uncaptured_value not in payload
traces = b"".join(
payload for path, payload, _ in received if path == "/v1/traces"
)
assert b"invalid-count" in traces
assert b"search-term" in traces
records = [
record
for path, payload, _ in received
if path == "/v1/logs"
for resource in ExportLogsServiceRequest.FromString(
payload
).resource_logs
for scope in resource.scope_logs
for record in scope.log_records
]
assert len(records) == 3
assert records[0].event_name == "fastapi.validation.failed"
assert records[2].event_name == "fastapi.validation.failed"
finally:
tracer.shutdown()
logger.shutdown()
@@ -0,0 +1,244 @@
"""Environment export remains active alongside independently configured SDKs."""
import pytest
from ._otlp import otlp_collector
from ._subprocess import run_in_subprocess
@pytest.mark.parametrize(
"mode",
[
"global",
"explicit",
"sentry-first",
"fastapi-first",
"sentry-classic",
"logfire",
"logfire-opt-out",
],
)
@run_in_subprocess
def test_environment_export_with_existing_integrations(mode):
import os
from fastapi import FastAPI
from fastapi.telemetry import TelemetryConfig, _runtime
from fastapi.testclient import TestClient
from opentelemetry import _logs, metrics, trace
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
ExportLogsServiceRequest,
)
from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import (
ExportMetricsServiceRequest,
)
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
ExportTraceServiceRequest,
)
from opentelemetry.proto.trace.v1.trace_pb2 import Span
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
with otlp_collector() as (base, received):
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = base + "/environment"
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "x-cloud=preserved"
os.environ["OTEL_BSP_SCHEDULE_DELAY"] = "60000"
os.environ["OTEL_BLRP_SCHEDULE_DELAY"] = "60000"
os.environ["OTEL_METRIC_EXPORT_INTERVAL"] = "60000"
settings: TelemetryConfig = {}
original_spans = InMemorySpanExporter()
original_logs = InMemoryLogRecordExporter()
original_metrics = InMemoryMetricReader()
if mode in ("global", "explicit"):
tp = TracerProvider(shutdown_on_exit=False)
tp.add_span_processor(SimpleSpanProcessor(original_spans))
mp = MeterProvider(
metric_readers=[original_metrics], shutdown_on_exit=False
)
lp = LoggerProvider(shutdown_on_exit=False)
lp.add_log_record_processor(SimpleLogRecordProcessor(original_logs))
if mode == "global":
trace.set_tracer_provider(tp)
metrics.set_meter_provider(mp)
_logs.set_logger_provider(lp)
else:
settings = {
"tracer_provider": tp,
"meter_provider": mp,
"logger_provider": lp,
}
elif mode.startswith("logfire"):
import logfire
logfire.configure(
send_to_logfire=False,
console=False,
additional_span_processors=[SimpleSpanProcessor(original_spans)],
metrics=logfire.MetricsOptions(additional_readers=[original_metrics]),
advanced=logfire.AdvancedOptions(
log_record_processors=[SimpleLogRecordProcessor(original_logs)]
),
)
settings["auto_configure"] = mode != "logfire-opt-out"
else:
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.otlp import OTLPIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from sentry_sdk.transport import Transport
items = []
class LocalTransport(Transport):
def capture_envelope(self, envelope):
items.extend(item.type for item in envelope.items)
if mode == "fastapi-first":
with TestClient(FastAPI()):
pass
integrations = (
[StarletteIntegration(), FastApiIntegration()]
if mode == "sentry-classic"
else [
OTLPIntegration(
collector_url=base + "/sentry/traces", setup_propagator=False
)
]
)
sentry_sdk.init(
dsn="https://public@example.invalid/1",
transport=LocalTransport,
default_integrations=False,
auto_enabling_integrations=False,
integrations=integrations,
traces_sample_rate=1.0,
send_client_reports=False,
)
app = FastAPI(telemetry=settings)
@app.get("/items/{item_id}")
def endpoint(item_id: int):
raise ValueError("environment export")
try:
for _ in range(2):
with TestClient(app, raise_server_exceptions=False) as client:
assert client.get("/items/1").status_code == 500
if mode.startswith("logfire"):
logfire.force_flush()
elif mode.startswith("sentry") or mode == "fastapi-first":
tracer_provider = trace.get_tracer_provider()
assert isinstance(tracer_provider, TracerProvider)
tracer_provider.force_flush()
sentry_sdk.flush()
def spans_at(path):
requests = [
ExportTraceServiceRequest.FromString(body)
for url, body, _ in received
if url == path
]
return [
span
for request in requests
for resource in request.resource_spans
for scope in resource.scope_spans
for span in scope.spans
]
environment_spans = spans_at("/environment/v1/traces")
multiplier = 2 if mode == "logfire" else 1
servers = [
span for span in environment_spans if span.kind == Span.SPAN_KIND_SERVER
]
assert len(servers) == 2 * multiplier, (mode, len(servers))
assert len({span.span_id for span in servers}) == 2
assert all(span.name == "GET /items/{item_id}" for span in servers)
if mode in ("global", "explicit"):
assert sorted(
(span.context.trace_id, span.context.span_id, span.name)
for span in original_spans.get_finished_spans()
) == sorted(
(
int.from_bytes(span.trace_id, "big"),
int.from_bytes(span.span_id, "big"),
span.name,
)
for span in environment_spans
)
if mode in ("sentry-first", "fastapi-first"):
sentry_spans = spans_at("/sentry/traces")
assert {span.span_id for span in sentry_spans} == {
span.span_id for span in environment_spans
}
if mode == "sentry-classic":
assert items.count("event") == 2, items
assert items.count("transaction") == 2, items
log_requests = [
ExportLogsServiceRequest.FromString(body)
for url, body, _ in received
if url == "/environment/v1/logs"
]
logs = [
record
for request in log_requests
for resource in request.resource_logs
for scope in resource.scope_logs
for record in scope.log_records
]
assert len(logs) == 2 * multiplier, (mode, len(logs))
assert all(
record.trace_id in {span.trace_id for span in servers}
for record in logs
)
metric_requests = [
ExportMetricsServiceRequest.FromString(body)
for url, body, _ in received
if url == "/environment/v1/metrics"
]
counts = [
point.count
for request in metric_requests
for resource in request.resource_metrics
for scope in resource.scope_metrics
for metric in scope.metrics
if metric.name == "http.server.request.duration"
for point in getattr(metric, metric.WhichOneof("data")).data_points
]
assert counts and counts[-1] == 2, counts
assert all(
headers.get("x-cloud") == "preserved"
for url, _, headers in received
if url.startswith("/environment/")
)
assert len(_runtime._configured) == (0 if mode == "logfire-opt-out" else 3)
_runtime._shutdown()
if mode in ("global", "explicit"):
# FastAPI's cleanup closes its own exporters, leaving the provider and
# previously installed vendor components usable.
count = len(original_spans.get_finished_spans())
with tp.get_tracer("vendor").start_as_current_span("still-running"):
pass
assert len(original_spans.get_finished_spans()) == count + 1
lp.get_logger("vendor").emit(body="still-running")
assert len(original_logs.get_finished_logs()) == 3
assert original_metrics.get_metrics_data() is not None
tp.shutdown()
mp.shutdown()
lp.shutdown()
finally:
_runtime._shutdown()
if mode.startswith("logfire"):
logfire.shutdown()
+249
View File
@@ -0,0 +1,249 @@
import pytest
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from opentelemetry import trace
from opentelemetry._logs import NoOpLoggerProvider, SeverityNumber
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
from .conftest import metric_points, server_spans
@pytest.mark.parametrize("sampled", [True, False])
def test_exception_is_logged_once_with_trace_context(telemetry, logs, sampled):
config, spans, reader = telemetry
if not sampled:
config["tracer_provider"].sampler = ALWAYS_OFF
app = FastAPI(telemetry=config)
contexts = []
error = ValueError("test failure")
@app.get("/items/{item_id}")
def endpoint(item_id: int):
contexts.append(trace.get_current_span().get_span_context())
raise error
assert (
TestClient(app, raise_server_exceptions=False).get("/items/1").status_code
== 500
)
(data,) = logs.get_finished_logs()
record = data.log_record
assert record.event_name == "http.server.request.exception"
assert record.timestamp is not None
assert record.timestamp <= record.observed_timestamp
assert (
data.instrumentation_scope.schema_url
== "https://opentelemetry.io/schemas/1.44.0"
)
assert record.exception is error
assert record.trace_id == contexts[0].trace_id
assert record.span_id != 0
if sampled:
(server,) = server_spans(spans)
assert record.span_id == server.context.span_id
assert record.severity_number == SeverityNumber.ERROR
assert record.attributes["http.route"] == "/items/{item_id}"
assert record.attributes["exception.type"] == "ValueError"
assert record.attributes["exception.message"] == "test failure"
assert "raise error" in record.attributes["exception.stacktrace"]
assert len(server_spans(spans)) == int(sampled)
@pytest.mark.parametrize("raises", [False, True])
def test_exclusion_applies_to_mounted_apps(telemetry, logs, raises):
config, spans, reader = telemetry
scopes = []
child = FastAPI(telemetry=config)
@child.get("/{name}")
def endpoint(*, request: Request, name: str):
scopes.append(request.scope)
if name == "excluded" and raises:
raise ValueError("excluded error")
return name
parent = FastAPI(
telemetry={
**config,
"exclude": lambda scope: scope["path"] == "/child/excluded",
}
)
parent.mount("/child", child)
client = TestClient(parent, raise_server_exceptions=False)
assert client.get("/child/excluded").status_code == (500 if raises else 200)
assert not spans.get_finished_spans()
assert not logs.get_finished_logs()
assert not metric_points(reader=reader)
assert "fastapi.telemetry" not in scopes[0]
assert client.get("/child/included").json() == "included"
(span,) = server_spans(spans)
assert span.attributes["http.route"] == "/child/{name}"
assert metric_points(reader=reader)[0].count == 1
def test_handled_exceptions_and_validation_are_not_error_logs(telemetry, logs):
config, spans, reader = telemetry
app = FastAPI(telemetry=config)
@app.exception_handler(ValueError)
async def handled(request, exc):
return JSONResponse({"detail": "handled"}, status_code=400)
@app.get("/items/{value}")
def endpoint(value: int):
if value == 1:
raise HTTPException(404)
raise ValueError("handled by application")
client = TestClient(app)
assert client.get("/items/no").status_code == 422
assert client.get("/items/1").status_code == 404
assert client.get("/items/2").status_code == 400
assert client.get("/missing").status_code == 404
(validation,) = logs.get_finished_logs()
assert validation.log_record.severity_number == SeverityNumber.WARN
assert validation.log_record.event_name == "fastapi.validation.failed"
assert validation.log_record.timestamp is not None
assert validation.log_record.timestamp <= validation.log_record.observed_timestamp
assert validation.log_record.exception is None
@pytest.mark.parametrize("stage", ["stream", "cleanup"])
def test_errors_after_response_started_are_logged(telemetry, logs, stage):
from fastapi import Depends
config, spans, reader = telemetry
app = FastAPI(telemetry=config)
def fail():
raise ValueError("after response started")
async def dependency():
yield
if stage == "cleanup":
fail()
@app.get("/", dependencies=[Depends(dependency)])
async def endpoint():
if stage == "stream":
async def stream():
yield "start"
fail()
return StreamingResponse(stream())
return "ok"
assert TestClient(app, raise_server_exceptions=False).get("/").status_code == 200
(data,) = logs.get_finished_logs()
(span,) = server_spans(spans)
assert data.log_record.trace_id == span.context.trace_id
assert (
"after response started" in data.log_record.attributes["exception.stacktrace"]
)
@pytest.mark.parametrize("mode", ["logs_only", "disabled", "noop", "excluded"])
def test_independent_log_configuration(telemetry, logs, mode):
config, spans, reader = telemetry
if mode == "logs_only":
config.update(tracing=False, metrics=False)
elif mode == "disabled":
config["logs"] = False
elif mode == "noop":
config["logger_provider"] = NoOpLoggerProvider()
else:
config["exclude"] = lambda scope: True
app = FastAPI(telemetry=config)
@app.get("/")
def endpoint():
raise ValueError("test")
assert TestClient(app, raise_server_exceptions=False).get("/").status_code == 500
records = logs.get_finished_logs()
assert len(records) == int(mode == "logs_only")
if records:
assert records[0].log_record.trace_id == 0
@pytest.mark.parametrize("status", [404, 503])
@pytest.mark.parametrize("sampled", [False, True])
def test_http_exception_records_status_without_exception_telemetry(
telemetry, logs, status, sampled
):
from opentelemetry.trace import StatusCode
config, spans, reader = telemetry
if not sampled:
config["tracer_provider"].sampler = ALWAYS_OFF
app = FastAPI(telemetry=config)
@app.get("/items/{item_id}")
def endpoint(item_id: int):
raise HTTPException(status, "private exception detail")
response = TestClient(app).get("/items/1")
assert response.status_code == status
assert response.json() == {"detail": "private exception detail"}
assert not logs.get_finished_logs()
(point,) = metric_points(reader=reader)
assert point.count == 1
assert point.attributes["http.response.status_code"] == status
assert point.attributes["http.route"] == "/items/{item_id}"
assert point.attributes.get("error.type") == ("503" if status == 503 else None)
finished = spans.get_finished_spans()
assert all(not span.events for span in finished)
if sampled:
(server,) = server_spans(spans)
assert server.attributes["http.response.status_code"] == status
assert server.status.status_code == (
StatusCode.ERROR if status == 503 else StatusCode.UNSET
)
else:
assert not finished
def test_exception_handler_failure_is_logged_once_as_unhandled(telemetry, logs):
config, _, _ = telemetry
failure = RuntimeError("handler failed")
app = FastAPI(telemetry=config)
@app.exception_handler(HTTPException)
async def handler(request, exc):
raise failure
@app.get("/")
async def endpoint():
raise HTTPException(404, "original")
assert TestClient(app, raise_server_exceptions=False).get("/").status_code == 500
(data,) = logs.get_finished_logs()
assert data.log_record.exception is failure
assert data.log_record.body == "Unhandled exception in FastAPI request"
class ApplicationError(Exception):
pass
def test_exception_type_matches_spans_metrics_and_logs(telemetry, logs):
config, spans, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/")
async def endpoint():
raise ApplicationError("failed")
assert TestClient(app, raise_server_exceptions=False).get("/").status_code == 500
(record,) = logs.get_finished_logs()
expected = f"{__name__}.ApplicationError"
assert record.log_record.attributes["exception.type"] == expected
failed_spans = [
span for span in spans.get_finished_spans() if span.status.is_ok is False
]
assert {span.name for span in failed_spans} == {"GET /", "fastapi.endpoint"}
assert all(span.attributes["error.type"] == expected for span in failed_spans)
(point,) = metric_points(reader=reader)
assert point.attributes["error.type"] == expected
+695
View File
@@ -0,0 +1,695 @@
import asyncio
from contextlib import asynccontextmanager
import anyio
import pytest
from fastapi import (
APIRouter,
BackgroundTasks,
Depends,
FastAPI,
HTTPException,
Request,
)
from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse
from fastapi.testclient import TestClient
from opentelemetry import baggage, propagate, trace
from opentelemetry.propagators import textmap
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
from opentelemetry.trace import SpanKind, StatusCode
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from starlette.middleware import Middleware
from starlette.types import Scope
from ._subprocess import run_in_subprocess
from .conftest import metric_points, server_spans
def test_request_context_and_span_attributes(telemetry):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
seen = []
@app.get("/items/{item_id}")
async def endpoint(item_id: int):
seen.append(trace.get_current_span().get_span_context())
return item_id
client = TestClient(app)
parent_trace = "0123456789abcdef0123456789abcdef"
parent_span = "0123456789abcdef"
response = client.get(
"/items/123?page=2&sig=signature-secret",
headers={
"traceparent": f"00-{parent_trace}-{parent_span}-01",
"authorization": "Bearer authorization-secret",
},
)
assert response.json() == 123
(span,) = server_spans(exporter)
assert span.kind == SpanKind.SERVER
assert span.name == "GET /items/{item_id}"
endpoint_span = next(
s for s in exporter.get_finished_spans() if s.name == "fastapi.endpoint"
)
assert endpoint_span.context == seen[0]
assert endpoint_span.parent.span_id == span.context.span_id
assert span.context.trace_id == int(parent_trace, 16)
assert span.parent.span_id == int(parent_span, 16)
assert span.status.status_code == StatusCode.UNSET
assert span.attributes["http.route"] == "/items/{item_id}"
assert span.attributes["http.response.status_code"] == 200
assert "signature-secret" not in repr(span.attributes)
assert "authorization-secret" not in repr(span.attributes)
assert span.attributes["url.path"] == "/items/123"
assert span.attributes["url.query"] == "page=2&sig=REDACTED"
points = metric_points(reader=reader)
assert len(points) == 1
assert points[0].count == 1
assert points[0].sum > 0
assert points[0].attributes["http.route"] == "/items/{item_id}"
assert points[0].explicit_bounds == (
0.005,
0.01,
0.025,
0.05,
0.075,
0.1,
0.25,
0.5,
0.75,
1,
2.5,
5,
7.5,
10,
)
assert (
metric_points(reader=reader, name="http.server.active_requests")[0].value == 0
)
assert not trace.get_current_span().get_span_context().is_valid
@pytest.mark.parametrize("split_headers", [False, True])
def test_propagation_headers(telemetry, split_headers):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
@app.get("/")
async def endpoint():
return dict(baggage.get_all())
trace_id = "0123456789abcdef0123456789abcdef"
parent_id = "0123456789abcdef"
headers = [("traceparent", f"00-{trace_id}-{parent_id}-01")]
for name, values in [
("tracestate", ["vendora=one", "vendorb=two"]),
("baggage", ["first=one", "second=two"]),
]:
headers.extend(
(name, value) for value in (values if split_headers else [",".join(values)])
)
response = TestClient(app).get("/", headers=headers)
assert response.json() == {"first": "one", "second": "two"}
(span,) = server_spans(exporter)
assert span.context.trace_id == int(trace_id, 16)
assert span.parent.span_id == int(parent_id, 16)
assert list(span.context.trace_state.items()) == [
("vendora", "one"),
("vendorb", "two"),
]
assert not baggage.get_all()
def test_custom_propagator_can_read_repeated_headers(telemetry, monkeypatch):
class HeaderPropagator(TraceContextTextMapPropagator):
def extract(self, carrier, context=None, getter=textmap.default_getter):
assert "x-custom-context" in getter.keys(carrier)
values = getter.get(carrier, "X-Custom-Context")
assert values is not None
assert values == ["one, two", "three"]
assert getter.get(carrier, "missing-header") is None
context = super().extract(carrier, context=context, getter=getter)
return baggage.set_baggage("custom", "|".join(values), context=context)
monkeypatch.setattr(propagate, "get_global_textmap", lambda: HeaderPropagator())
config, _, _ = telemetry
app = FastAPI(telemetry=config)
@app.get("/")
async def endpoint():
return baggage.get_baggage("custom")
response = TestClient(app).get(
"/", headers=[("x-custom-context", "one, two"), ("x-custom-context", "three")]
)
assert response.json() == "one, two|three"
assert not baggage.get_all()
@pytest.mark.parametrize(
"path,status,route,error",
[
("/ok", 200, "/ok", False),
("/missing", 404, None, False),
("/validation/no", 422, "/validation/{value}", False),
("/validation/1", 200, "/validation/{value}", False),
("/fail", 500, "/fail", True),
("/handled", 503, "/handled", True),
("/ok/", 307, "/ok", False),
],
)
def test_http_statuses(telemetry, path, status, route, error):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/ok")
def ok():
return "ok"
@app.get("/validation/{value}")
def validation(value: int):
return value
@app.get("/fail")
def fail():
raise ValueError("endpoint failed")
@app.get("/handled")
async def handled():
raise HTTPException(503, "Unavailable")
assert (
TestClient(app, raise_server_exceptions=False)
.get(path, follow_redirects=False)
.status_code
== status
)
(span,) = server_spans(exporter)
assert span.attributes["http.response.status_code"] == status
assert span.attributes.get("http.route") == route
assert (span.status.status_code == StatusCode.ERROR) == error
assert "endpoint failed" not in repr(span.attributes)
assert span.events == ()
assert sum(p.count for p in metric_points(reader=reader)) == 1
def test_custom_error_handler_sees_span(telemetry):
config, exporter, reader = telemetry
seen = []
async def error_handler(request, exc):
span = trace.get_current_span()
seen.append(span.is_recording())
span.set_attribute("handled", True)
return JSONResponse({"error": "handled"}, status_code=500)
app = FastAPI(telemetry=config, exception_handlers={500: error_handler})
@app.get("/")
def endpoint():
raise ValueError("endpoint failed")
assert TestClient(app, raise_server_exceptions=False).get("/").json() == {
"error": "handled"
}
(span,) = server_spans(exporter)
assert seen == [True]
assert span.attributes["handled"] is True
assert span.attributes["error.type"] == "ValueError"
def test_route_prefix_and_dynamic_mount(telemetry):
config, exporter, reader = telemetry
parent = FastAPI(telemetry=config, root_path="/proxy")
child = FastAPI()
router = APIRouter(prefix="/v1")
@router.get("/items/{item_id}")
def endpoint(item_id: int):
return item_id
child.include_router(router, prefix="/api")
parent.mount("/tenants/{tenant}", child)
response = TestClient(parent).get("/proxy/tenants/acme/api/v1/items/4")
assert response.json() == 4
(span,) = server_spans(exporter)
assert (
span.attributes["http.route"]
== "/proxy/tenants/{tenant}/api/v1/items/{item_id}"
)
assert span.name == "GET /proxy/tenants/{tenant}/api/v1/items/{item_id}"
assert span.attributes["url.path"] == "/proxy/tenants/acme/api/v1/items/4"
assert "acme" not in repr(metric_points(reader=reader)[0].attributes)
assert sum(p.count for p in metric_points(reader=reader)) == 1
@pytest.mark.parametrize("nested", [False, True])
@pytest.mark.parametrize("route_type", ["fastapi", "starlette"])
def test_included_router_redirect_template(telemetry, nested, route_type):
config, exporter, reader = telemetry
router = APIRouter()
def endpoint(request: Request):
return PlainTextResponse("ok")
if route_type == "fastapi":
router.add_api_route("/items/{item_id}/", endpoint)
else:
router.add_route("/items/{item_id}/", endpoint)
if nested:
outer = APIRouter()
outer.include_router(router, prefix="/v1")
router = outer
child = FastAPI()
child.include_router(router, prefix="/api")
app = FastAPI(telemetry=config)
app.mount("/tenants/{tenant}", child)
prefix = "/api" + ("/v1" if nested else "")
path = "/tenants/acme" + prefix + "/items/1"
template = "/tenants/{tenant}" + prefix + "/items/{item_id}/"
client = TestClient(app)
response = client.get(path, follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"] == "http://testserver" + path + "/"
(span,) = server_spans(exporter)
assert span.name == "GET " + template
assert span.attributes["http.route"] == template
(point,) = metric_points(reader=reader)
assert point.count == 1
assert point.attributes["http.route"] == template
assert client.get(path + "/").text == "ok"
def test_method_not_allowed_and_docs(telemetry):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/items/{value}")
def endpoint(value: str):
return value
client = TestClient(app)
assert client.post("/items/x").status_code == 405
assert client.get("/docs").status_code == 200
assert [s.attributes["http.route"] for s in exporter.get_finished_spans()] == [
"/items/{value}",
"/docs",
]
assert client.get("/items/x").json() == "x"
def test_stream_completes_before_background_and_cleanup(telemetry):
config, exporter, reader = telemetry
seen = []
app = FastAPI(telemetry=config)
async def dependency():
yield
seen.append(("cleanup", len(server_spans(exporter))))
async def background():
seen.append(("background", len(server_spans(exporter))))
@app.get("/", dependencies=[Depends(dependency)])
async def endpoint(tasks: BackgroundTasks):
tasks.add_task(background)
async def stream():
yield "one"
assert not server_spans(exporter)
await anyio.sleep(0)
yield "two"
return StreamingResponse(stream())
assert TestClient(app).get("/").text == "onetwo"
assert seen == [("background", 1), ("cleanup", 1)]
assert sum(p.count for p in metric_points(reader=reader)) == 1
@pytest.mark.parametrize(
"tracing,metering", [(True, False), (False, True), (False, False)]
)
def test_independent_signals(telemetry, tracing, metering):
config, exporter, reader = telemetry
config["tracing"], config["metrics"] = tracing, metering
app = FastAPI(telemetry=config)
TestClient(app).get("/missing")
assert len(exporter.get_finished_spans()) == int(tracing)
assert sum(p.count for p in metric_points(reader=reader)) == int(metering)
def test_configuration_is_copied_for_each_app(telemetry):
config, exporter, reader = telemetry
original = config.copy()
enabled = FastAPI(telemetry=config)
assert config == original
config["tracing"] = False
disabled = FastAPI(telemetry=config)
config["tracing"] = True
assert TestClient(enabled).get("/").status_code == 404
assert TestClient(disabled).get("/").status_code == 404
assert len(server_spans(exporter)) == 1
assert sum(point.count for point in metric_points(reader=reader)) == 2
def test_unsampled_requests_still_record_metrics(telemetry):
config, exporter, reader = telemetry
config["tracer_provider"].sampler = ALWAYS_OFF
TestClient(FastAPI(telemetry=config)).get("/missing")
assert not exporter.get_finished_spans()
assert metric_points(reader=reader)[0].count == 1
def test_exclusion(telemetry):
config, exporter, reader = telemetry
config["exclude"] = lambda scope: scope["path"] == "/health"
client = TestClient(FastAPI(telemetry=config))
client.get("/health")
assert not exporter.get_finished_spans()
assert not metric_points(reader=reader)
client.get("/other")
assert len(exporter.get_finished_spans()) == 1
@pytest.mark.parametrize("raises", [False, True])
def test_middleware_response_or_error(telemetry, raises):
config, exporter, reader = telemetry
class CustomMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if raises:
raise RuntimeError("middleware failed")
await PlainTextResponse("middleware")(scope, receive, send)
app = FastAPI(telemetry=config, middleware=[Middleware(CustomMiddleware)])
response = TestClient(app, raise_server_exceptions=False).get("/")
assert response.status_code == (500 if raises else 200)
(span,) = server_spans(exporter)
assert "http.route" not in span.attributes
if raises:
assert span.attributes["error.type"] == "RuntimeError"
@pytest.mark.parametrize(
"kind", ["trailers", "disconnect", "cancel", "pathsend", "incomplete", "send_error"]
)
def test_asgi_lifecycle(telemetry, kind):
from fastapi.telemetry._asgi import NativeTelemetry
config, exporter, reader = telemetry
sent = []
scope: Scope = {
"type": "http",
"method": "GET",
"scheme": "http",
"path": "/",
"headers": [],
}
async def receive():
return {"type": "http.disconnect"}
async def send(message):
if kind == "send_error":
raise OSError("closed")
sent.append(message)
async def app(scope, receive, send):
if kind == "disconnect":
await receive()
return
if kind == "cancel":
raise asyncio.CancelledError()
if kind == "incomplete":
return
await send(
{
"type": "http.response.start",
"status": 200,
"trailers": kind == "trailers",
}
)
if kind == "pathsend":
await send({"type": "http.response.pathsend", "path": "/tmp/example"})
else:
await send({"type": "http.response.body", "body": b"ok"})
if kind == "trailers":
assert not exporter.get_finished_spans()
await send(
{"type": "http.response.trailers", "headers": [], "more_trailers": True}
)
assert not exporter.get_finished_spans()
await send({"type": "http.response.trailers", "headers": []})
async def run():
await NativeTelemetry(FastAPI(telemetry=config)._telemetry)(
app=app, scope=scope, receive=receive, send=send
)
if kind == "cancel":
with pytest.raises(asyncio.CancelledError):
asyncio.run(run())
elif kind == "send_error":
with pytest.raises(OSError):
asyncio.run(run())
else:
asyncio.run(run())
(span,) = server_spans(exporter)
assert (span.status.status_code == StatusCode.ERROR) == (
kind in {"cancel", "disconnect", "incomplete", "send_error"}
)
assert metric_points(reader=reader)[0].count == 1
assert (
metric_points(reader=reader, name="http.server.active_requests")[0].value == 0
)
assert "fastapi.telemetry" not in scope
def test_multiple_lifespans_borrowed_providers(telemetry):
config, exporter, reader = telemetry
lifecycle = []
@asynccontextmanager
async def lifespan(app):
lifecycle.append("start")
yield {"state": True}
lifecycle.append("stop")
first = FastAPI(telemetry=config, lifespan=lifespan)
second = FastAPI(telemetry=config)
for app in [first, second, first]:
with TestClient(app) as client:
assert client.get("/").status_code == 404
assert lifecycle == ["start", "stop", "start", "stop"]
assert len(exporter.get_finished_spans()) == 3
assert sum(p.count for p in metric_points(reader=reader)) == 3
@pytest.mark.parametrize(
"method",
[
"CONNECT",
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT",
"QUERY",
"TRACE",
],
)
def test_known_http_methods(telemetry, method):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.api_route("/items/{value}", methods=[method])
def endpoint(value: str):
return value
assert TestClient(app).request(method, "/items/1").status_code == 200
(span,) = server_spans(exporter)
assert span.name == f"{method} /items/{{value}}"
assert span.attributes["http.request.method"] == method
assert "http.request.method_original" not in span.attributes
assert metric_points(reader=reader)[0].attributes["http.request.method"] == method
assert (
metric_points(reader=reader, name="http.server.active_requests")[0].attributes[
"http.request.method"
]
== method
)
@pytest.mark.parametrize(
"path,status,span_name",
[("/items/1", 405, "HTTP /items/{value}"), ("/missing", 404, "HTTP")],
)
def test_unknown_method_has_bounded_span_name(telemetry, path, status, span_name):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/items/{value}")
def endpoint(value: str):
return value
assert TestClient(app).request("PRIVATE_METHOD", path).status_code == status
(span,) = server_spans(exporter)
assert span.name == span_name
assert span.attributes["http.request.method"] == "_OTHER"
assert span.attributes["http.request.method_original"] == "PRIVATE_METHOD"
for name in ("http.server.request.duration", "http.server.active_requests"):
attributes = metric_points(reader=reader, name=name)[0].attributes
assert attributes["http.request.method"] == "_OTHER"
assert "http.request.method_original" not in attributes
assert TestClient(app).get("/items/1").json() == "1"
@pytest.mark.parametrize("matched", [True, False])
@pytest.mark.parametrize(
"known_methods,method,expected",
[
(" QUERY , PROPFIND , ", "QUERY", "QUERY"),
(" QUERY , PROPFIND , ", "PROPFIND", "PROPFIND"),
("QUERY,PROPFIND", "GET", "_OTHER"),
("query", "QUERY", "_OTHER"),
("CUSTOM", "CUSTOM", "CUSTOM"),
("", "QUERY", "QUERY"),
(" ", "QUERY", "QUERY"),
],
)
def test_known_http_methods_override(
telemetry, monkeypatch, known_methods, method, expected, matched
):
monkeypatch.setenv("OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS", known_methods)
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.api_route("/items/{value}", methods=[method])
def endpoint(value: str):
return value
path = "/items/1" if matched else "/missing"
assert TestClient(app).request(method, path).status_code == (
200 if matched else 404
)
(span,) = server_spans(exporter)
span_method = "HTTP" if expected == "_OTHER" else method
assert span.name == span_method + (" /items/{value}" if matched else "")
assert span.attributes["http.request.method"] == expected
if expected == "_OTHER":
assert span.attributes["http.request.method_original"] == method
else:
assert "http.request.method_original" not in span.attributes
for name in ("http.server.request.duration", "http.server.active_requests"):
attributes = metric_points(reader=reader, name=name)[0].attributes
assert attributes["http.request.method"] == expected
assert "http.request.method_original" not in attributes
def test_frontend_and_static_templates(telemetry, tmp_path):
from fastapi.staticfiles import StaticFiles
config, exporter, reader = telemetry
(tmp_path / "index.html").write_text("index")
(tmp_path / "script.js").write_text("script")
app = FastAPI(telemetry=config)
app.mount("/static", StaticFiles(directory=tmp_path))
router = APIRouter()
router.frontend("/ui", directory=tmp_path, fallback="index.html")
app.include_router(router, prefix="/app")
client = TestClient(app)
assert client.get("/static/script.js").text == "script"
assert (
client.get("/app/ui/arbitrary/path", headers={"accept": "text/html"}).text
== "index"
)
assert [s.attributes["http.route"] for s in exporter.get_finished_spans()] == [
"/static/{path}",
"/app/ui/{path}",
]
def test_included_mount_template(telemetry):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
child = FastAPI()
@child.get("/items/{value}")
def endpoint(value: str):
return value
router = APIRouter()
router.mount("/tenants/{tenant}", child)
app.include_router(router, prefix="/api")
assert TestClient(app).get("/api/tenants/acme/items/1").json() == "1"
(span,) = server_spans(exporter)
assert span.attributes["http.route"] == "/api/tenants/{tenant}/items/{value}"
@pytest.mark.parametrize("explicit_proxy", [False, True])
@run_in_subprocess
def test_late_global_providers_enable_existing_app(explicit_proxy):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from opentelemetry import metrics, trace
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
app = FastAPI(
telemetry={
"tracer_provider": trace.get_tracer_provider(),
"meter_provider": metrics.get_meter_provider(),
}
if explicit_proxy
else None
)
client = TestClient(app)
assert client.get("/").status_code == 404
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
reader = InMemoryMetricReader()
trace.set_tracer_provider(provider)
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
assert client.get("/").status_code == 404
assert len(exporter.get_finished_spans()) == 1
assert reader.get_metrics_data() is not None
def test_base_http_middleware_preserves_context_and_route(telemetry):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
seen = []
@app.middleware("http")
async def middleware(request, call_next):
seen.append(trace.get_current_span().get_span_context().span_id)
return await call_next(request)
@app.get("/items/{item_id}")
async def endpoint(item_id: int):
seen.append(trace.get_current_span().get_span_context().span_id)
return item_id
assert TestClient(app).get("/items/1").json() == 1
(span,) = server_spans(exporter)
endpoint_span = next(
s for s in exporter.get_finished_spans() if s.name == "fastapi.endpoint"
)
assert seen == [span.context.span_id, endpoint_span.context.span_id]
assert endpoint_span.parent.span_id == span.context.span_id
assert span.attributes["http.route"] == "/items/{item_id}"
+351
View File
@@ -0,0 +1,351 @@
"""Published integration compatibility, isolated because SDKs patch globals."""
import os
import pytest
from ._subprocess import run_in_subprocess
@pytest.mark.parametrize("mode", ["app", "global", "late", "uninstrument"])
@run_in_subprocess
def test_current_contrib(mode):
import os
os.environ.update({"OTEL_SEMCONV_STABILITY_OPT_IN": "http"})
import fastapi
from fastapi.testclient import TestClient
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from opentelemetry.trace import SpanKind
native = InMemorySpanExporter()
legacy = InMemorySpanExporter()
p1, p2 = TracerProvider(), TracerProvider()
p1.add_span_processor(SimpleSpanProcessor(native))
p2.add_span_processor(SimpleSpanProcessor(legacy))
r1, r2 = InMemoryMetricReader(), InMemoryMetricReader()
m1, m2 = MeterProvider(metric_readers=[r1]), MeterProvider(metric_readers=[r2])
hooks = []
def server_request_hook(span, scope):
hooks.append(scope["path"])
if mode == "global":
FastAPIInstrumentor().instrument(
tracer_provider=p2,
meter_provider=m2,
server_request_hook=server_request_hook,
exclude_spans=["send", "receive"],
)
app = fastapi.FastAPI(telemetry={"tracer_provider": p1, "meter_provider": m1})
@app.get("/items/{value}")
def endpoint(value: int):
return value
client = TestClient(app)
if mode == "late":
assert client.get("/items/1").json() == 1
if mode != "global":
FastAPIInstrumentor.instrument_app(
app,
tracer_provider=p2,
meter_provider=m2,
server_request_hook=server_request_hook,
exclude_spans=["send", "receive"],
)
assert client.get("/items/2").json() == 2
if mode == "late":
# Current contrib cannot replace an already-built stack. Native remains live.
assert len(native.get_finished_spans()) == 8
assert not legacy.get_finished_spans()
else:
assert not native.get_finished_spans()
assert (
len([s for s in legacy.get_finished_spans() if s.kind == SpanKind.SERVER])
== 1
)
assert hooks == ["/items/2"]
assert r1.get_metrics_data() is None
if mode == "uninstrument":
FastAPIInstrumentor.uninstrument_app(app)
assert client.get("/items/3").json() == 3
assert len(native.get_finished_spans()) == 4
@app.websocket("/ws")
async def websocket_endpoint(websocket: fastapi.WebSocket):
await websocket.accept()
await websocket.close()
native.clear()
legacy.clear()
with client.websocket_connect("/ws"):
pass
if mode in ("late", "uninstrument"):
assert len(native.get_finished_spans()) == 3
assert not legacy.get_finished_spans()
else:
assert not native.get_finished_spans()
assert (
len(
[
span
for span in legacy.get_finished_spans()
if span.kind == SpanKind.SERVER
]
)
== 1
)
assert hooks[-1] == "/ws"
@pytest.mark.parametrize("order", ["before", "after"])
@run_in_subprocess
def test_current_logfire(order):
import logfire
from fastapi import FastAPI, WebSocket
from fastapi.testclient import TestClient
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from opentelemetry.trace import SpanKind
legacy, native = InMemorySpanExporter(), InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(native))
def configure():
logfire.configure(
send_to_logfire=False,
console=False,
metrics=False,
additional_span_processors=[SimpleSpanProcessor(legacy)],
)
if order == "before":
configure()
app = FastAPI(telemetry={"tracer_provider": provider})
if order == "after":
configure()
@app.get("/items/{value}")
def endpoint(value: int):
return value
mapped = []
def mapper(request, attributes):
mapped.append(request.url.path)
return attributes
logfire.instrument_fastapi(app, request_attributes_mapper=mapper, extra_spans=True)
assert TestClient(app).get("/items/5").json() == 5
spans = legacy.get_finished_spans()
assert len([s for s in spans if s.kind == SpanKind.SERVER]) == 1, spans
assert any("arguments" in s.name for s in spans)
assert any(
s.attributes is not None
and s.attributes.get("code.function")
in (endpoint.__name__, endpoint.__qualname__)
for s in spans
)
assert mapped == ["/items/5"]
assert not native.get_finished_spans()
@app.websocket("/ws/{value}")
async def websocket_endpoint(*, websocket: WebSocket, value: int):
await websocket.accept()
await websocket.close()
legacy.clear()
with TestClient(app).websocket_connect("/ws/5"):
pass
assert not native.get_finished_spans()
assert (
len(
[
span
for span in legacy.get_finished_spans()
if span.kind == SpanKind.SERVER
]
)
== 1
)
assert mapped[-1] == "/ws/5"
@pytest.mark.parametrize("sampling", [0.0, 1.0])
@run_in_subprocess
def test_current_sentry(sampling):
import sentry_sdk
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.testclient import TestClient
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from sentry_sdk.transport import Transport
items = []
class LocalTransport(Transport):
def capture_envelope(self, envelope):
items.extend(item.type for item in envelope.items)
sentry_sdk.init(
dsn="https://public@example.invalid/1",
transport=LocalTransport,
default_integrations=False,
auto_enabling_integrations=False,
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=sampling,
send_client_reports=False,
)
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
logs = InMemoryLogRecordExporter()
logger = LoggerProvider()
logger.add_log_record_processor(SimpleLogRecordProcessor(logs))
app = FastAPI(telemetry={"tracer_provider": provider, "logger_provider": logger})
@app.get("/items/{value}")
def endpoint(value: int):
if value == 2:
raise ValueError("test")
if value == 3:
raise HTTPException(503, "handled")
return value
client = TestClient(app, raise_server_exceptions=False)
assert client.get("/items/1").status_code == 200
assert client.get("/items/2").status_code == 500
assert client.get("/items/3").status_code == 503
sentry_sdk.flush()
assert items.count("event") == 2, items
(unhandled,) = logs.get_finished_logs()
assert isinstance(unhandled.log_record.exception, ValueError)
assert items.count("transaction") == (3 if sampling else 0), items
assert len(exporter.get_finished_spans()) == 10
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
raise ValueError("websocket failed")
with pytest.raises(ValueError, match="websocket failed"):
with client.websocket_connect("/ws"):
pass # pragma: no cover
sentry_sdk.flush()
assert items.count("event") == 3, items
assert items.count("transaction") == (4 if sampling else 0), items
assert len(exporter.get_finished_spans()) == 13
assert len(logs.get_finished_logs()) == 2
@run_in_subprocess
def test_api_only_and_no_implicit_sdk_import():
import sys
from importlib.abc import MetaPathFinder
class BlockSDK(MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(("opentelemetry.sdk", "opentelemetry.exporter")):
raise AssertionError(
f"Unexpected optional import: {fullname}"
) # pragma: no cover
sys.meta_path.insert(0, BlockSDK())
from fastapi import FastAPI, WebSocket
from fastapi.testclient import TestClient
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.close()
with TestClient(app) as client:
assert client.get("/").status_code == 404
with client.websocket_connect("/ws"):
pass
assert not any(name.startswith("opentelemetry.sdk") for name in sys.modules)
def test_inactive_sentry_does_not_disable_native(telemetry):
import sentry_sdk
from fastapi import FastAPI
from fastapi.testclient import TestClient
config, exporter, reader = telemetry
assert sentry_sdk.get_client().get_integration("fastapi") is None
assert TestClient(FastAPI(telemetry=config)).get("/").status_code == 404
assert len(exporter.get_finished_spans()) == 1
@run_in_subprocess
def test_logfire_global_provider_without_fastapi_instrumentor():
import logfire
from fastapi import FastAPI
from fastapi.testclient import TestClient
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
exporter = InMemorySpanExporter()
logfire.configure(
send_to_logfire=False,
console=False,
metrics=False,
additional_span_processors=[SimpleSpanProcessor(exporter)],
)
app = FastAPI()
@app.get("/")
def endpoint():
return "ok"
assert TestClient(app).get("/").json() == "ok"
spans = exporter.get_finished_spans()
assert {span.name for span in spans} == {
"GET /",
"fastapi.dependencies",
"fastapi.endpoint",
"fastapi.serialization",
}
assert all(
span.instrumentation_scope is not None
and span.instrumentation_scope.name == "fastapi"
for span in spans
)
def test_environment_isolation_removes_export_credentials(monkeypatch):
from .conftest import remove_export_environment
for name in ["OTEL_EXPORTER_OTLP_HEADERS", "LOGFIRE_TOKEN", "SENTRY_DSN"]:
monkeypatch.setenv(name, "test-only")
remove_export_environment(monkeypatch)
assert not any(
name.startswith(("OTEL_", "LOGFIRE_", "SENTRY_")) for name in os.environ
)
@@ -0,0 +1,316 @@
"""Integration prototypes using standard OpenTelemetry SDK extension points."""
import pytest
from ._subprocess import run_in_subprocess
@run_in_subprocess
def test_logfire_native_spans_and_exception_logs():
import logfire
from fastapi import Depends, FastAPI, WebSocket, routing
from fastapi.telemetry import get_telemetry_data
from fastapi.testclient import TestClient
from opentelemetry import trace
from opentelemetry.sdk._logs import LogRecordProcessor
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
original = (
routing.get_websocket_app,
routing.get_request_handler,
routing.run_endpoint_function,
routing.solve_dependencies,
)
spans = InMemorySpanExporter()
logs = InMemoryLogRecordExporter()
dependency_value = object()
observed = []
class Arguments(SpanProcessor):
def on_start(self, span, parent_context=None):
if span.name != "fastapi.endpoint":
return
data = get_telemetry_data(parent_context)
assert data is not None
connection = data.request or data.websocket
assert connection is not None
assert data.values is not None
if connection.url.path in ("/error", "/ws/error"):
return
assert data.values["service"] is dependency_value
assert data.values["item_id"] == 42
observed.append(connection.url.path)
span.set_attribute("test.item_id", data.values["item_id"])
class Validation(LogRecordProcessor):
def on_emit(self, log_record):
record = log_record.log_record
if record.event_name != "fastapi.validation.failed":
return
data = get_telemetry_data(record.context)
assert data is not None
connection = data.request or data.websocket
assert connection is not None
assert data.errors is not None
assert data.errors[0]["input"] == "invalid-item-id"
observed.append(connection.url.path)
record.attributes["test.error_types"] = tuple(
error["type"] for error in data.errors
)
def shutdown(self):
pass
def force_flush(self, timeout_millis=30000):
return True
logfire.configure(
send_to_logfire=False,
console=False,
metrics=False,
additional_span_processors=[Arguments(), SimpleSpanProcessor(spans)],
advanced=logfire.AdvancedOptions(
log_record_processors=[Validation(), SimpleLogRecordProcessor(logs)]
),
)
app = FastAPI()
contexts = []
def dependency():
assert trace.get_current_span().get_span_context().is_valid
return dependency_value
@app.get("/sync")
def sync_endpoint(*, item_id: int, service=Depends(dependency)):
return "ok"
@app.get("/async")
async def async_endpoint(*, item_id: int, service=Depends(dependency)):
return "ok"
@app.get("/error")
async def error_endpoint():
contexts.append(trace.get_current_span().get_span_context())
raise ValueError("native error")
@app.websocket("/ws")
async def websocket_endpoint(
*, websocket: WebSocket, item_id: int, service=Depends(dependency)
):
await websocket.accept()
await websocket.send_text("ok")
await websocket.close()
@app.websocket("/ws/error")
async def websocket_error(websocket: WebSocket):
contexts.append(trace.get_current_span().get_span_context())
await websocket.accept()
raise ValueError("native error")
client = TestClient(app, raise_server_exceptions=False)
assert client.get("/sync?item_id=42").json() == "ok"
assert client.get("/async?item_id=42").json() == "ok"
assert client.get("/error").status_code == 500
assert client.get("/async?item_id=invalid-item-id").status_code == 422
from starlette.websockets import WebSocketDisconnect
with client.websocket_connect("/ws?item_id=42") as websocket:
assert websocket.receive_text() == "ok"
with pytest.raises(ValueError, match="native error"):
with client.websocket_connect("/ws/error"):
pass # pragma: no cover
with pytest.raises(WebSocketDisconnect) as caught:
with client.websocket_connect("/ws?item_id=invalid-item-id"):
pass # pragma: no cover
assert caught.value.code == 1008
logfire.force_flush()
finished = spans.get_finished_spans()
assert len([s for s in finished if s.kind == trace.SpanKind.SERVER]) == 7
assert (
len(
[
s
for s in finished
if s.name == "fastapi.endpoint"
and s.attributes is not None
and s.attributes.get("logfire.span_type") != "pending_span"
]
)
== 5
)
data, validation, websocket_error_log, websocket_validation = (
logs.get_finished_logs()
)
assert websocket_error_log.log_record.exception is not None
assert websocket_error_log.log_record.trace_id == contexts[1].trace_id
assert websocket_validation.log_record.event_name == "fastapi.validation.failed"
assert websocket_validation.log_record.attributes is not None
assert "test.error_types" in websocket_validation.log_record.attributes
assert len([span for span in finished if span.name == "WS /ws"]) == 2
assert validation.log_record.event_name == "fastapi.validation.failed"
assert validation.log_record.attributes is not None
assert "test.error_types" in validation.log_record.attributes
assert observed == ["/sync", "/async", "/async", "/ws", "/ws"]
arguments = [s for s in finished if s.attributes and "test.item_id" in s.attributes]
assert arguments
assert all(s.attributes and s.attributes["test.item_id"] == 42 for s in arguments)
assert get_telemetry_data() is None
assert data.log_record.attributes is not None
assert data.log_record.attributes["exception.message"] == "native error"
assert data.log_record.trace_id == contexts[0].trace_id
assert original == (
routing.get_websocket_app,
routing.get_request_handler,
routing.run_endpoint_function,
routing.solve_dependencies,
)
@pytest.mark.parametrize("sampled", [True, False])
@run_in_subprocess
def test_sentry_standard_log_processor(sampled):
import sentry_sdk
from fastapi import FastAPI, WebSocket, routing
from fastapi.telemetry import get_telemetry_data
from fastapi.testclient import TestClient
from opentelemetry import trace
from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, ALWAYS_ON
from sentry_sdk.integrations.otlp import OTLPIntegration
from sentry_sdk.transport import Transport
original = (
routing.get_websocket_app,
routing.get_request_handler,
routing.run_endpoint_function,
routing.solve_dependencies,
)
items = []
class LocalTransport(Transport):
def capture_envelope(self, envelope):
items.extend(
item.payload.json for item in envelope.items if item.type == "event"
)
class SentryErrors(LogRecordProcessor):
def on_emit(self, log_record):
if (
log_record.instrumentation_scope.name == "fastapi"
and log_record.log_record.exception is not None
):
data = get_telemetry_data(log_record.log_record.context)
assert data is not None
connection = data.request or data.websocket
assert connection is not None
assert data.values is not None
def enrich(event, hint):
event["request"] = {
"url": str(connection.url.replace(query=None)),
"query_string": str(connection.query_params),
"data": data.body,
}
if data.request is not None:
event["request"]["method"] = data.request.method
return event
with sentry_sdk.new_scope() as scope:
scope.add_event_processor(enrich)
sentry_sdk.capture_exception(log_record.log_record.exception)
def shutdown(self):
pass
def force_flush(self, timeout_millis=30000):
return True
sentry_sdk.init(
dsn="https://public@example.invalid/1",
transport=LocalTransport,
default_integrations=False,
auto_enabling_integrations=False,
integrations=[
OTLPIntegration(setup_otlp_traces_exporter=False, setup_propagator=False)
],
send_client_reports=False,
)
logger = LoggerProvider()
logger.add_log_record_processor(SentryErrors())
tracer = TracerProvider(sampler=ALWAYS_ON if sampled else ALWAYS_OFF)
exporter = InMemorySpanExporter()
tracer.add_span_processor(SimpleSpanProcessor(exporter))
app = FastAPI(telemetry={"tracer_provider": tracer, "logger_provider": logger})
contexts = []
@app.post("/sync")
def sync_endpoint(payload: dict):
contexts.append(trace.get_current_span().get_span_context())
raise ValueError("sync error")
@app.post("/async")
async def async_endpoint(payload: dict):
contexts.append(trace.get_current_span().get_span_context())
raise ValueError("async error")
@app.websocket("/ws")
async def websocket_endpoint(*, websocket: WebSocket, item_id: int):
contexts.append(trace.get_current_span().get_span_context())
await websocket.accept()
raise ValueError("websocket error")
client = TestClient(app, raise_server_exceptions=False)
assert client.post("/sync?item_id=1", json={"value": "sync"}).status_code == 500
assert client.post("/async?item_id=2", json={"value": "async"}).status_code == 500
with pytest.raises(ValueError, match="websocket error"):
with client.websocket_connect("/ws?item_id=3"):
pass # pragma: no cover
assert logger.force_flush()
sentry_sdk.flush()
assert len(items) == 3
assert [item["request"] for item in items] == [
{
"method": "POST",
"url": "http://testserver/sync",
"query_string": "item_id=1",
"data": {"value": "sync"},
},
{
"method": "POST",
"url": "http://testserver/async",
"query_string": "item_id=2",
"data": {"value": "async"},
},
{
"url": "ws://testserver/ws",
"query_string": "item_id=3",
"data": None,
},
]
assert [item["contexts"]["trace"]["trace_id"] for item in items] == [
format(ctx.trace_id, "032x") for ctx in contexts
]
assert all(item["exception"]["values"][0]["stacktrace"]["frames"] for item in items)
assert bool(exporter.get_finished_spans()) == sampled
assert original == (
routing.get_websocket_app,
routing.get_request_handler,
routing.run_endpoint_function,
routing.solve_dependencies,
)
logger.shutdown()
tracer.shutdown()
+93
View File
@@ -0,0 +1,93 @@
import threading
from typing import Annotated
import pytest
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
from opentelemetry import baggage, trace
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.trace import SpanKind, StatusCode
@pytest.mark.parametrize("sync", [False, True])
def test_native_operations_and_worker_context(telemetry, sync):
config, exporter, reader = telemetry
started = {}
executed = {}
class Processor(SpanProcessor):
def on_start(self, span, parent_context=None):
started[span.name] = (threading.get_ident(), span.get_span_context())
config["tracer_provider"].add_span_processor(Processor())
app = FastAPI(telemetry=config)
def dependency():
executed["dependency"] = trace.get_current_span().get_span_context()
return "private argument"
def implementation(value: Annotated[str, Depends(dependency)]):
executed["endpoint"] = (
threading.get_ident(),
trace.get_current_span().get_span_context(),
)
assert value == "private argument"
return baggage.get_baggage("example")
async def async_endpoint(value: Annotated[str, Depends(dependency)]):
return implementation(value)
app.get("/")(implementation if sync else async_endpoint)
assert (
TestClient(app).get("/", headers={"baggage": "example=value"}).json() == "value"
)
assert started["fastapi.endpoint"] == executed["endpoint"]
assert started["fastapi.dependencies"][1] == executed["dependency"]
assert (
started["fastapi.endpoint"][0] != started["fastapi.dependencies"][0]
) == sync
spans = exporter.get_finished_spans()
server = next(s for s in spans if s.kind == SpanKind.SERVER)
children = [s for s in spans if s is not server]
assert {s.name for s in children} == {
"fastapi.dependencies",
"fastapi.endpoint",
"fastapi.serialization",
}
assert all(s.parent.span_id == server.context.span_id for s in children)
assert all(s.context.trace_id == server.context.trace_id for s in children)
assert all("code.function.name" in s.attributes for s in children)
assert "private argument" not in repr([s.attributes for s in spans])
assert baggage.get_baggage("example") is None
assert not trace.get_current_span().get_span_context().is_valid
@pytest.mark.parametrize(
"failure", ["dependency", "endpoint", "serialization", "handled"]
)
def test_operation_errors(telemetry, failure):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
async def dependency():
if failure == "dependency":
raise ValueError("dependency failure")
@app.get("/", dependencies=[Depends(dependency)], response_model=int)
async def endpoint():
if failure == "endpoint":
raise ValueError("endpoint failure")
if failure == "handled":
raise HTTPException(400)
return "not an integer"
response = TestClient(app, raise_server_exceptions=False).get("/")
assert response.status_code == (400 if failure == "handled" else 500)
spans = exporter.get_finished_spans()
if failure == "handled":
assert all(s.status.status_code == StatusCode.UNSET for s in spans)
else:
name = "dependencies" if failure == "dependency" else failure
failed = next(s for s in spans if s.name == f"fastapi.{name}")
assert failed.status.status_code == StatusCode.ERROR
assert all(not s.events for s in spans)
+648
View File
@@ -0,0 +1,648 @@
import asyncio
import logging
import threading
from contextlib import asynccontextmanager
from unittest.mock import Mock
import pytest
from fastapi import FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.telemetry import _runtime as runtime
from fastapi.testclient import TestClient
from ._otlp import otlp_collector
from ._subprocess import run_in_subprocess
def test_otlp_collector_does_not_resolve_hostname(monkeypatch):
# Reverse DNS during HTTPServer binding caused macOS CI timeouts.
lookup = Mock()
monkeypatch.setattr("socket.getfqdn", lookup)
with otlp_collector():
pass
lookup.assert_not_called()
@pytest.mark.parametrize("base_path", ["", "/collector/"])
@run_in_subprocess
def test_real_otlp_export_and_repeated_lifespans(base_path):
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.telemetry import _runtime
from fastapi.testclient import TestClient
from opentelemetry import _logs, trace
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
ExportLogsServiceRequest,
)
from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import (
ExportMetricsServiceRequest,
)
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
ExportTraceServiceRequest,
)
from opentelemetry.proto.trace.v1.trace_pb2 import Span
with otlp_collector() as (base, received):
prefix = base_path.rstrip("/")
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = base + base_path
os.environ["OTEL_SERVICE_NAME"] = "native-test"
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "x-test=value"
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "test.resource=example"
os.environ["OTEL_BSP_SCHEDULE_DELAY"] = "60000"
os.environ["OTEL_BLRP_SCHEDULE_DELAY"] = "60000"
os.environ["OTEL_METRIC_EXPORT_INTERVAL"] = "60000"
seen = []
@asynccontextmanager
async def lifespan(app):
seen.append(trace.get_tracer_provider())
yield
app = FastAPI(lifespan=lifespan)
@app.get("/items/{value}")
def endpoint(value: int):
_logs.get_logger("test").emit(body="request processed")
return value
try:
for _ in range(2):
with TestClient(app) as client:
assert client.get("/items/3").json() == 3
assert len(_runtime._owned) == 3
assert seen[0] is seen[1]
assert all(headers.get("x-test") == "value" for _, _, headers in received)
traces = [
ExportTraceServiceRequest.FromString(body)
for path, body, _ in received
if path == prefix + "/v1/traces"
]
spans = [
span
for request in traces
for resource in request.resource_spans
for scope in resource.scope_spans
for span in scope.spans
]
assert len(spans) == 8
servers = [span for span in spans if span.kind == Span.SPAN_KIND_SERVER]
assert len(servers) == 2
assert all(span.name == "GET /items/{value}" for span in servers)
metric_requests = [
ExportMetricsServiceRequest.FromString(body)
for path, body, _ in received
if path == prefix + "/v1/metrics"
]
assert metric_requests
resources = metric_requests[-1].resource_metrics
resource_attributes = {
a.key: a.value.string_value for a in resources[0].resource.attributes
}
assert resource_attributes["service.name"] == "native-test"
assert resource_attributes["test.resource"] == "example"
histograms = [
metric.histogram
for resource in resources
for scope in resource.scope_metrics
for metric in scope.metrics
if metric.name == "http.server.request.duration"
]
assert histograms[0].data_points[0].count == 2
log_requests = [
ExportLogsServiceRequest.FromString(body)
for path, body, _ in received
if path == prefix + "/v1/logs"
]
records = [
record
for request in log_requests
for resource in request.resource_logs
for scope in resource.scope_logs
for record in scope.log_records
]
assert len(records) == 2
assert all(
record.body.string_value == "request processed" for record in records
)
finally:
_runtime._shutdown()
@pytest.mark.parametrize(
"env",
[
{
"OTEL_TRACES_EXPORTER": "",
"OTEL_METRICS_EXPORTER": "",
"OTEL_LOGS_EXPORTER": "",
"OTEL_EXPORTER_OTLP_PROTOCOL": "",
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "",
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "",
},
{
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "",
},
],
)
def test_empty_environment_uses_defaults(monkeypatch, env):
monkeypatch.setenv(
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:4318/collector/"
)
for name, value in env.items():
monkeypatch.setenv(name, value)
for signal in ("TRACES", "METRICS", "LOGS"):
assert runtime._export_endpoint(signal) == (
f"http://127.0.0.1:4318/collector/v1/{signal.lower()}"
)
@pytest.mark.parametrize("protocol", ["http/protobuf", "grpc"])
def test_empty_signal_protocol_uses_general_protocol(monkeypatch, protocol):
monkeypatch.setenv(
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://127.0.0.1:4318/v1/traces"
)
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", protocol)
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "")
if protocol == "http/protobuf":
assert runtime._export_endpoint("TRACES") == "http://127.0.0.1:4318/v1/traces"
else:
with pytest.raises(FastAPIError, match="http/protobuf"):
runtime._export_endpoint("TRACES")
@pytest.mark.parametrize("signal", ["TRACES", "METRICS", "LOGS"])
def test_signal_endpoint_overrides_general_endpoint(monkeypatch, signal):
endpoint = "http://127.0.0.1:4318/custom/"
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "invalid")
monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal}_ENDPOINT", endpoint)
assert runtime._export_endpoint(signal) == endpoint
@pytest.mark.parametrize(
"env,message",
[
({"OTEL_EXPORTER_OTLP_ENDPOINT": "not-a-url"}, "absolute HTTP"),
(
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:1",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
},
"http/protobuf",
),
(
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:1",
"OTEL_TRACES_EXPORTER": "console",
},
"otlp or none",
),
],
)
def test_invalid_configuration_warns_without_disabling_providers(
monkeypatch, caplog, telemetry, env, message
):
for name, value in env.items():
monkeypatch.setenv(name, value)
config, exporter, _ = telemetry
events = []
@asynccontextmanager
async def lifespan(app):
events.append("startup")
yield
events.append("shutdown")
app = FastAPI(telemetry=config, lifespan=lifespan)
@app.get("/health")
async def health():
return {"status": "ok"}
with TestClient(app) as client:
assert client.get("/health").json() == {"status": "ok"}
assert events == ["startup", "shutdown"]
assert exporter.get_finished_spans()
assert not runtime._owned
assert len(caplog.records) == 1
assert caplog.records[0].levelno == logging.WARNING
assert message in caplog.messages[0]
@pytest.mark.parametrize("stage", ["startup", "shutdown"])
def test_application_lifespan_errors_still_propagate(monkeypatch, stage):
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "not-a-url")
@asynccontextmanager
async def lifespan(app):
if stage == "startup":
raise RuntimeError("application startup failed")
yield
raise RuntimeError("application shutdown failed")
with pytest.raises(RuntimeError, match=f"application {stage} failed"):
with TestClient(FastAPI(lifespan=lifespan)) as client:
assert client.get("/").status_code == 404
@pytest.mark.parametrize(
"env",
[
{},
{"OTEL_SERVICE_NAME": "no-endpoint"},
{"OTEL_TRACES_EXPORTER": "otlp", "OTEL_METRICS_EXPORTER": "otlp"},
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:1",
"OTEL_TRACES_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
"OTEL_LOGS_EXPORTER": "none",
},
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:1",
"OTEL_SDK_DISABLED": "true",
},
],
)
@run_in_subprocess
def test_no_implicit_export(env):
import os
os.environ.update(env)
from fastapi import FastAPI
from fastapi.telemetry import _runtime
from fastapi.testclient import TestClient
with TestClient(FastAPI()) as client:
assert client.get("/").status_code == 404
assert _runtime._owned == []
@run_in_subprocess
def test_missing_sdk_warns_without_preventing_startup():
import os
os.environ.update(
{"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://127.0.0.1:1/metrics"}
)
import sys
from importlib.abc import MetaPathFinder
class BlockSDK(MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(("opentelemetry.sdk", "opentelemetry.exporter")):
raise ImportError("SDK absent")
sys.meta_path.insert(0, BlockSDK())
from unittest import TestCase
from fastapi import FastAPI
from fastapi.testclient import TestClient
with TestCase().assertLogs("fastapi", level="WARNING") as logs:
with TestClient(FastAPI()) as client:
assert client.get("/").status_code == 404
assert len(logs.records) == 1
assert logs.records[0].levelno == logging.WARNING
assert "fastapi[opentelemetry]" in logs.output[0]
assert not runtime._owned
@run_in_subprocess
def test_external_globals_are_unchanged_when_auto_configuration_is_disabled():
import os
os.environ.update({"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:1"})
from fastapi import FastAPI
from fastapi.telemetry import _runtime
from fastapi.testclient import TestClient
from opentelemetry import _logs, metrics, trace
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
exporter = InMemorySpanExporter()
tp = TracerProvider()
tp.add_span_processor(SimpleSpanProcessor(exporter))
mp = MeterProvider()
lp = LoggerProvider()
_logs.set_logger_provider(lp)
trace.set_tracer_provider(tp)
metrics.set_meter_provider(mp)
for _ in range(2):
with TestClient(FastAPI(telemetry={"auto_configure": False})) as client:
client.get("/")
assert trace.get_tracer_provider() is tp
assert metrics.get_meter_provider() is mp
assert _logs.get_logger_provider() is lp
assert not _runtime._owned
assert len(exporter.get_finished_spans()) == 2
def test_auto_configuration_opt_out(monkeypatch):
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "invalid")
with TestClient(FastAPI(telemetry={"auto_configure": False})) as client:
assert client.get("/").status_code == 404
def test_owned_flush_failure_does_not_break_shutdown(monkeypatch, caplog):
calls = []
class FailingProvider:
def force_flush(self):
raise RuntimeError("flush failed")
def shutdown(self):
raise RuntimeError("shutdown failed")
class Provider:
def force_flush(self):
calls.append("flush")
def shutdown(self):
calls.append("shutdown")
monkeypatch.setattr(runtime, "_owned", [FailingProvider(), Provider()])
with TestClient(FastAPI()) as client:
assert client.get("/").status_code == 404
runtime._shutdown()
runtime._shutdown()
assert calls == ["flush", "shutdown"]
assert caplog.text.count("FastAPI telemetry cleanup failed") == 2
@pytest.mark.parametrize(
"message_type",
[
"lifespan.startup.failed",
"lifespan.shutdown.failed",
"lifespan.shutdown.complete",
],
)
def test_lifespan_flush_runs_off_event_loop(monkeypatch, message_type):
calls = []
flush_threads = []
event_loop_thread = threading.get_ident()
class Provider:
def force_flush(self):
flush_threads.append(threading.get_ident())
calls.append("flush")
def shutdown(self):
calls.append("shutdown")
monkeypatch.setattr(runtime, "_owned", [Provider()])
async def app(scope, receive, send):
assert await receive() == {"type": "lifespan.startup"}
await send({"type": message_type})
async def receive():
return {"type": "lifespan.startup"}
async def send(message):
calls.append(message["type"])
asyncio.run(
runtime.lifespan(
config=FastAPI()._telemetry, app=app, scope={}, receive=receive, send=send
)
)
assert calls == ["flush", message_type]
assert len(flush_threads) == 1
assert flush_threads[0] != event_loop_thread
runtime._shutdown()
runtime._shutdown()
assert calls == ["flush", message_type, "shutdown"]
def test_registration_provider_prefers_public_metric_reader(monkeypatch):
from logfire._internal.metrics import ProxyMeterProvider
from opentelemetry.metrics import NoOpMeterProvider
provider = NoOpMeterProvider()
proxy = ProxyMeterProvider(provider=provider)
assert runtime._registration_provider(proxy) is provider
monkeypatch.setattr(proxy, "add_metric_reader", lambda reader: None, raising=False)
assert runtime._registration_provider(proxy) is proxy
@pytest.mark.parametrize("wrapped_meter", [False, True])
def test_concurrent_provider_owner_wins(monkeypatch, wrapped_meter):
from opentelemetry import _logs, metrics, trace
from opentelemetry.sdk import _logs as sdk_logs
from opentelemetry.sdk import metrics as sdk_metrics
from opentelemetry.sdk import trace as sdk_trace
stopped = []
winners = []
for name, api, sdk, cls_name in [
("tracer", trace, sdk_trace, "TracerProvider"),
("meter", metrics, sdk_metrics, "MeterProvider"),
("logger", _logs, sdk_logs, "LoggerProvider"),
]:
original = getattr(sdk, cls_name)
winner = original(shutdown_on_exit=False)
winners.append(winner)
class Provider(original):
def shutdown(self, _name=name):
stopped.append(_name)
super().shutdown()
if wrapped_meter and name == "meter":
from logfire._internal.metrics import ProxyMeterProvider
winner = ProxyMeterProvider(winner)
current = [getattr(api, f"get_{name}_provider")()]
monkeypatch.setattr(sdk, cls_name, Provider)
monkeypatch.setattr(api, f"get_{name}_provider", lambda c=current: c[0])
monkeypatch.setattr(
api,
f"set_{name}_provider",
lambda provider, c=current, w=winner: c.__setitem__(0, w),
)
monkeypatch.setattr(runtime, "_owned", [])
monkeypatch.setattr(runtime, "_configured", [])
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:1")
runtime._configure_from_environment(FastAPI()._telemetry)
assert stopped == ["tracer", "meter", "logger"]
assert [provider for _, provider in runtime._configured] == winners
assert len(runtime._owned) == 3
assert all(component not in winners for component in runtime._owned)
runtime._shutdown()
for winner in winners:
winner.shutdown()
@pytest.mark.skipif(
not hasattr(__import__("os"), "fork"), reason="POSIX worker lifecycle"
)
@run_in_subprocess
def test_environment_export_initializes_after_fork():
import os
import threading
from http.server import BaseHTTPRequestHandler
from fastapi import FastAPI
from fastapi.telemetry import _runtime
from fastapi.testclient import TestClient
from tests.test_telemetry._otlp import _CollectorServer
received = []
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
self.rfile.read(int(self.headers["Content-Length"]))
received.append(self.path)
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
pass
server = _CollectorServer(("127.0.0.1", 0), Handler)
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = (
f"http://127.0.0.1:{server.server_port}/traces"
)
# Importing and constructing before fork must not create providers.
app = FastAPI()
assert not _runtime._owned
pid = os.fork()
if pid == 0:
try:
with TestClient(app) as client:
assert client.get("/").status_code == 404
assert len(_runtime._owned) == 1
_runtime._shutdown()
except BaseException: # pragma: no cover
os._exit(1)
os._exit(0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
_, status = os.waitpid(pid, 0)
assert status == 0, status
assert received == ["/traces"], received
assert not _runtime._owned
server.shutdown()
server.server_close()
@run_in_subprocess
def test_real_otlp_exception_export_without_traces_or_metrics():
import os
from fastapi import FastAPI
from fastapi.telemetry import _runtime
from fastapi.testclient import TestClient
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
ExportLogsServiceRequest,
)
with otlp_collector() as (base, received):
os.environ["OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"] = base + "/logs"
os.environ["OTEL_SERVICE_NAME"] = "errors-only"
app = FastAPI()
@app.get("/items/{item_id}")
def endpoint(item_id: int):
raise ValueError("exported exception")
try:
for _ in range(2):
with TestClient(app, raise_server_exceptions=False) as client:
assert client.get("/items/1").status_code == 500
assert len(_runtime._owned) == 1
assert all(path == "/logs" for path, _, _ in received)
requests = [
ExportLogsServiceRequest.FromString(body) for _, body, _ in received
]
records = [
record
for request in requests
for resource in request.resource_logs
for scope in resource.scope_logs
for record in scope.log_records
]
assert len(records) == 2
for record in records:
attributes = {a.key: a.value.string_value for a in record.attributes}
assert attributes["exception.type"] == "ValueError"
assert attributes["exception.message"] == "exported exception"
assert attributes["http.route"] == "/items/{item_id}"
assert "in endpoint" in attributes["exception.stacktrace"]
assert (
"ValueError: exported exception"
in attributes["exception.stacktrace"]
)
finally:
_runtime._shutdown()
@pytest.mark.parametrize("name", ["tracer", "meter", "logger"])
def test_unsupported_provider_warns_without_preventing_startup(
monkeypatch, caplog, name
):
from opentelemetry._logs import NoOpLoggerProvider
from opentelemetry.metrics import NoOpMeterProvider
from opentelemetry.trace import NoOpTracerProvider
signal, provider = {
"tracer": ("TRACES", NoOpTracerProvider),
"meter": ("METRICS", NoOpMeterProvider),
"logger": ("LOGS", NoOpLoggerProvider),
}[name]
monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal}_ENDPOINT", "http://127.0.0.1:1")
app = FastAPI(telemetry={f"{name}_provider": provider()})
with TestClient(app) as client:
assert client.get("/").status_code == 404
assert "does not support adding an OTLP exporter" in caplog.text
assert "auto_configure" in caplog.text
assert not runtime._owned
@pytest.mark.parametrize("error_type", [ValueError, AttributeError])
def test_failed_registration_closes_new_exporter(monkeypatch, caplog, error_type):
from opentelemetry.exporter.otlp.proto.http import trace_exporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
stopped = []
class Exporter(InMemorySpanExporter):
def __init__(self, *, endpoint):
super().__init__()
def shutdown(self):
stopped.append(True)
super().shutdown()
provider = TracerProvider(shutdown_on_exit=False)
def fail(component):
raise error_type("registration failed")
monkeypatch.setattr(provider, "add_span_processor", fail)
monkeypatch.setattr(trace_exporter, "OTLPSpanExporter", Exporter)
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://127.0.0.1:1")
app = FastAPI(telemetry={"tracer_provider": provider})
with TestClient(app) as client:
assert client.get("/").status_code == 404
assert "registration failed" in caplog.text
assert stopped == [True]
provider.shutdown()
@@ -0,0 +1,163 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .conftest import metric_points, server_spans
@pytest.mark.parametrize(
"scheme,host,expected",
[
(
"http",
"public.example",
{"server.address": "public.example", "server.port": 80},
),
(
"https",
"public.example",
{"server.address": "public.example", "server.port": 443},
),
(
"http",
"public.example:8443",
{"server.address": "public.example", "server.port": 8443},
),
(
"http",
"[2001:db8::1]:8443",
{"server.address": "2001:db8::1", "server.port": 8443},
),
("http", "[2001:db8::1]", {"server.address": "2001:db8::1", "server.port": 80}),
("http", "public.example:invalid", {}),
("http", "[invalid", {}),
("http", "public.example:99999", {}),
("http", "user:password@public.example", {}),
("http", "public.example/path", {}),
("http", "", {}),
("custom", "public.example", {"server.address": "public.example"}),
],
)
def test_request_authority_is_only_on_spans(telemetry, scheme, host, expected):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/")
async def endpoint():
return "ok"
async def asgi(scope, receive, send):
scope["scheme"] = scheme
await app(scope, receive, send)
client = TestClient(asgi, base_url="http://listener:8000")
assert client.get("/", headers={"host": host}).json() == "ok"
(span,) = server_spans(exporter)
assert {
key: value
for key, value in span.attributes.items()
if key.startswith("server.")
} == expected
for name in ("http.server.request.duration", "http.server.active_requests"):
(point,) = metric_points(reader=reader, name=name)
assert not any(key.startswith("server.") for key in point.attributes)
def test_server_fallback_without_host_header(telemetry):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
async def asgi(scope, receive, send):
scope["headers"] = [
(name, value) for name, value in scope["headers"] if name != b"host"
]
await app(scope, receive, send)
assert TestClient(asgi, base_url="http://listener:8000").get("/").status_code == 404
(span,) = server_spans(exporter)
assert span.attributes["server.address"] == "listener"
assert span.attributes["server.port"] == 8000
@pytest.mark.parametrize(
"query,expected",
[
("", None),
(
"?tag=one&tag=two&empty=&flag",
"tag=one&tag=two&empty=&flag=",
),
(
"?X-Amz-Signature=secret&X-Amz-Credential=secret&X-Amz-Security-Token=secret&sig=secret&X-Goog-Signature=secret",
"X-Amz-Signature=REDACTED&X-Amz-Credential=REDACTED&X-Amz-Security-Token=REDACTED&sig=REDACTED&X-Goog-Signature=REDACTED",
),
(
"?%70age=2&q=fastapi%20tutorial&sort%20by=name",
"page=2&q=fastapi+tutorial&sort+by=name",
),
(
"?%73ig=secret&q=hello%26world",
"sig=REDACTED&q=hello%26world",
),
(
"?x-amz-signature=keep&x-amz-credential=keep&x-amz-security-token=keep&SIG=keep&x-goog-signature=keep",
"x-amz-signature=keep&x-amz-credential=keep&x-amz-security-token=keep&SIG=keep&x-goog-signature=keep",
),
(
"?sig=one&sig=two&sig=&q=hello+world",
"sig=REDACTED&sig=REDACTED&sig=REDACTED&q=hello+world",
),
],
)
def test_url_attributes_do_not_split_metrics(telemetry, query, expected):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.get("/items/{item_id}")
async def endpoint(item_id: int):
return item_id
client = TestClient(app)
for item_id in (1, 2):
assert (
client.get(
f"/items/{item_id}{query}", headers={"host": f"host-{item_id}.example"}
).json()
== item_id
)
first, second = server_spans(exporter)
assert first.attributes["url.path"] == "/items/1"
assert second.attributes["url.path"] == "/items/2"
assert first.attributes.get("url.query") == expected
assert second.attributes.get("url.query") == expected
(point,) = metric_points(reader=reader)
assert point.count == 2
assert "url.path" not in point.attributes
assert "url.query" not in point.attributes
assert point.attributes["http.route"] == "/items/{item_id}"
@pytest.mark.parametrize("metrics", [None, False, True])
def test_active_requests_follow_metrics_setting(telemetry, metrics):
config, _, reader = telemetry
if metrics is not None:
config["metrics"] = metrics
app = FastAPI(telemetry=config)
seen = []
@app.get("/")
async def endpoint():
seen.extend(
point.value
for point in metric_points(
reader=reader, name="http.server.active_requests"
)
)
return "ok"
assert TestClient(app).get("/").json() == "ok"
active = metrics is not False
assert seen == ([1] if active else [])
points = metric_points(reader=reader, name="http.server.active_requests")
assert [point.value for point in points] == ([0] if active else [])
assert bool(metric_points(reader=reader)) == active
+292
View File
@@ -0,0 +1,292 @@
import pytest
from fastapi import APIRouter, Depends, FastAPI, WebSocket, WebSocketException
from fastapi.telemetry import get_telemetry_data
from fastapi.testclient import TestClient
from opentelemetry import context
from opentelemetry._logs import SeverityNumber
from opentelemetry.sdk._logs import LogRecordProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
from opentelemetry.trace import StatusCode
from starlette.websockets import WebSocketDisconnect
from .conftest import server_spans
@pytest.mark.parametrize("operation_spans", [False, True])
def test_connection_context_route_and_data(telemetry, operation_spans):
config, exporter, reader = telemetry
config["operation_spans"] = operation_spans
app = FastAPI(telemetry=config)
child = FastAPI(telemetry=config)
router = APIRouter()
service = object()
saved = []
def dependency(websocket: WebSocket):
data = get_telemetry_data()
assert data is not None
assert data.websocket is websocket
assert data.request is None
assert data.body is None
assert data.values is None
return service
@router.websocket("/rooms/{room}")
async def endpoint(*, websocket: WebSocket, room: int, value=Depends(dependency)):
data = get_telemetry_data()
assert data is not None
assert data.values is not None
assert data.values["value"] is service
assert data.values["room"] == room
assert data.errors == []
saved.append(context.get_current())
await websocket.accept()
for _ in range(2):
await websocket.send_text(await websocket.receive_text())
await websocket.close()
child.include_router(router, prefix="/api")
app.mount("/tenants/{tenant}", child)
with TestClient(app).websocket_connect(
"/tenants/acme/api/rooms/42",
headers={"traceparent": "00-" + "1" * 32 + "-" + "2" * 16 + "-01"},
) as websocket:
websocket.send_text("private payload")
assert websocket.receive_text() == "private payload"
assert not server_spans(exporter)
websocket.send_text("second message")
assert websocket.receive_text() == "second message"
(span,) = server_spans(exporter)
assert span.name == "WS /tenants/{tenant}/api/rooms/{room}"
assert span.parent.span_id == int("2" * 16, 16)
assert span.context.trace_id == int("1" * 32, 16)
assert span.attributes["network.protocol.name"] == "websocket"
assert span.attributes["url.scheme"] == "ws"
assert span.status.status_code == StatusCode.UNSET
assert not any(key.startswith("http.request.") for key in span.attributes)
assert "http.response.status_code" not in span.attributes
assert "network.protocol.version" not in span.attributes
assert reader.get_metrics_data() is None
assert len(exporter.get_finished_spans()) == (3 if operation_spans else 1)
assert "private payload" not in repr(exporter.get_finished_spans())
assert all(get_telemetry_data(saved_context) is None for saved_context in saved)
assert get_telemetry_data() is None
@pytest.mark.parametrize("code", [1000, 1001, 1006])
def test_client_disconnect(telemetry, logs, code):
config, exporter, reader = telemetry
app = FastAPI(telemetry=config)
@app.websocket("/ws")
async def endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.receive_text()
with pytest.raises(WebSocketDisconnect) as caught:
with TestClient(app).websocket_connect("/ws") as websocket:
websocket.close(code=code)
assert caught.value.code == code
(server,) = server_spans(exporter)
assert server.status.status_code == (
StatusCode.ERROR if code == 1006 else StatusCode.UNSET
)
assert len(logs.get_finished_logs()) == int(code == 1006)
for span in exporter.get_finished_spans():
assert not span.events
assert span.status.status_code == (
StatusCode.ERROR
if code == 1006 and span.name != "fastapi.dependencies"
else StatusCode.UNSET
)
assert reader.get_metrics_data() is None
@pytest.mark.parametrize("mode", ["sampled", "unsampled", "logs_only", "logs_disabled"])
@pytest.mark.parametrize("stage", ["dependency", "endpoint", "cleanup"])
def test_unexpected_exception(telemetry, logs, mode, stage):
config, exporter, reader = telemetry
if mode == "unsampled":
config["tracer_provider"].sampler = ALWAYS_OFF
elif mode == "logs_only":
config["tracing"] = False
elif mode == "logs_disabled":
config["logs"] = False
app = FastAPI(telemetry=config)
failure = ValueError("websocket failed")
saved = []
async def dependency():
saved.append(context.get_current())
if stage == "dependency":
raise failure
yield
if stage == "cleanup":
raise failure
@app.websocket("/ws", dependencies=[Depends(dependency)])
async def endpoint(websocket: WebSocket):
await websocket.accept()
if stage == "endpoint":
raise failure
await websocket.close()
with pytest.raises(ValueError, match="websocket failed"):
with TestClient(app).websocket_connect("/ws"):
pass # pragma: no cover
records = logs.get_finished_logs()
assert len(records) == int(mode != "logs_disabled")
if records:
record = records[0].log_record
assert record.event_name == "fastapi.websocket.exception"
assert record.timestamp is not None
assert record.timestamp <= record.observed_timestamp
assert record.exception is failure
assert record.severity_number == SeverityNumber.ERROR
assert record.body == "Unhandled exception in FastAPI WebSocket connection"
assert record.attributes["http.route"] == "/ws"
assert bool(record.trace_id) == (mode != "logs_only")
assert get_telemetry_data(record.context) is None
if mode in ("sampled", "logs_disabled"):
(server,) = server_spans(exporter)
assert server.status.status_code == StatusCode.ERROR
assert server.attributes["error.type"] == "ValueError"
if records:
assert records[0].log_record.span_id == server.context.span_id
else:
assert not exporter.get_finished_spans()
assert reader.get_metrics_data() is None
assert all(get_telemetry_data(saved_context) is None for saved_context in saved)
@pytest.mark.parametrize("tracing", [False, True])
def test_validation_data(telemetry, logs, tracing):
config, exporter, reader = telemetry
config["tracing"] = tracing
app = FastAPI(telemetry=config)
observed = []
class Observe(LogRecordProcessor):
def on_emit(self, log_record):
data = get_telemetry_data(log_record.log_record.context)
assert data is not None
assert data.websocket is not None
assert data.request is None
assert data.body is None
assert data.errors is not None
assert data.errors[0]["input"] == "private-invalid-input"
observed.append(log_record.log_record.context)
def shutdown(self):
pass
def force_flush(self, timeout_millis=30000):
return True
config["logger_provider"].add_log_record_processor(Observe())
@app.websocket("/ws/{value}")
async def endpoint(*, websocket: WebSocket, value: int):
pytest.fail(
"Validation should fail before the endpoint runs"
) # pragma: no cover
with pytest.raises(WebSocketDisconnect) as caught:
with TestClient(app).websocket_connect("/ws/private-invalid-input"):
pass # pragma: no cover
assert caught.value.code == 1008
assert config["logger_provider"].force_flush()
(record,) = logs.get_finished_logs()
assert record.log_record.event_name == "fastapi.validation.failed"
assert record.log_record.severity_number == SeverityNumber.WARN
assert record.log_record.attributes == {
"http.route": "/ws/{value}",
"fastapi.validation.error_count": 1,
}
assert record.log_record.exception is None
assert "private-invalid-input" not in repr(record.log_record.attributes)
assert len(observed) == 1
assert get_telemetry_data(observed[0]) is None
if tracing:
assert all(
span.status.status_code == StatusCode.UNSET
for span in exporter.get_finished_spans()
)
assert reader.get_metrics_data() is None
@pytest.mark.parametrize("excluded", [False, True])
def test_disabled_and_excluded_connections(telemetry, logs, excluded):
config, exporter, reader = telemetry
if excluded:
config["exclude"] = lambda scope: scope["type"] == "websocket"
else:
config.update(tracing=False, logs=False)
app = FastAPI(telemetry=config)
child = FastAPI(telemetry=config)
app.mount("/child", child)
@child.websocket("/ws")
async def endpoint(websocket: WebSocket):
assert get_telemetry_data() is None
await websocket.accept()
await websocket.close()
with TestClient(app).websocket_connect("/child/ws"):
pass
assert not exporter.get_finished_spans()
assert not logs.get_finished_logs()
assert reader.get_metrics_data() is None
def test_handled_websocket_exception(telemetry, logs):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
@app.websocket("/ws")
async def endpoint(websocket: WebSocket):
raise WebSocketException(code=1008)
with pytest.raises(WebSocketDisconnect) as caught:
with TestClient(app).websocket_connect("/ws"):
pass # pragma: no cover
assert caught.value.code == 1008
assert len(server_spans(exporter)) == 1
assert not logs.get_finished_logs()
assert all(
span.status.status_code == StatusCode.UNSET
for span in exporter.get_finished_spans()
)
def test_concurrent_connections_keep_local_data_separate(telemetry):
config, exporter, _ = telemetry
app = FastAPI(telemetry=config)
saved = []
observed = []
@app.websocket("/ws/{name}")
async def endpoint(*, websocket: WebSocket, name: str):
data = get_telemetry_data()
assert data is not None
await websocket.accept()
await websocket.receive_text()
assert get_telemetry_data() is data
assert data.values is not None
assert data.values["name"] == name
assert data.websocket is websocket
saved.append(context.get_current())
observed.append(data)
await websocket.send_text(name)
await websocket.close()
with TestClient(app) as client:
with client.websocket_connect("/ws/first") as first:
with client.websocket_connect("/ws/second") as second:
second.send_text("go")
assert second.receive_text() == "second"
first.send_text("go")
assert first.receive_text() == "first"
assert observed[0] is not observed[1]
assert len(server_spans(exporter)) == 2
assert all(get_telemetry_data(saved_context) is None for saved_context in saved)
Whitespace-only changes.
@@ -0,0 +1,8 @@
import pytest
from tests.test_telemetry.conftest import remove_export_environment
@pytest.fixture(autouse=True)
def clean_environment(monkeypatch):
remove_export_environment(monkeypatch)
@@ -0,0 +1,8 @@
from fastapi.testclient import TestClient
def test_default_example():
from docs_src.opentelemetry.tutorial001_py310 import app
with TestClient(app) as client:
assert client.get("/items/1").json() == {"item_id": 1}
@@ -0,0 +1,28 @@
import json
from io import StringIO
from fastapi.testclient import TestClient
def test_console_provider_example(monkeypatch):
from opentelemetry.sdk.trace import export
output = StringIO()
exporter = export.ConsoleSpanExporter(
out=output, formatter=lambda span: span.to_json(indent=None) + "\n"
)
monkeypatch.setattr(export, "ConsoleSpanExporter", lambda: exporter)
from docs_src.opentelemetry.tutorial002_py310 import app, tracer_provider
try:
with TestClient(app) as client:
assert client.get("/items/2").json() == {"item_id": 2}
assert tracer_provider.force_flush()
spans = [json.loads(line) for line in output.getvalue().splitlines()]
assert len(spans) == 4
span = next(span for span in spans if span["kind"] == "SpanKind.SERVER")
assert span["name"] == "GET /items/{item_id}"
assert span["kind"] == "SpanKind.SERVER"
assert span["attributes"]["http.route"] == "/items/{item_id}"
finally:
tracer_provider.shutdown()
@@ -0,0 +1,22 @@
from fastapi.testclient import TestClient
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
def test_disable_operation_spans_example(monkeypatch):
exporter = InMemorySpanExporter()
provider = TracerProvider(shutdown_on_exit=False)
provider.add_span_processor(SimpleSpanProcessor(exporter))
monkeypatch.setattr(trace, "get_tracer_provider", lambda: provider)
from docs_src.opentelemetry.tutorial003_py310 import app
try:
with TestClient(app) as client:
assert client.get("/items/3").json() == {"item_id": 3}
(span,) = exporter.get_finished_spans()
assert span.kind == trace.SpanKind.SERVER
assert span.name == "GET /items/{item_id}"
finally:
provider.shutdown()
Generated
+343 -269
View File
File diff suppressed because it is too large. Load diff