django-api-opentelemetry 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_api_opentelemetry-0.1.0/PKG-INFO +298 -0
- django_api_opentelemetry-0.1.0/README.md +275 -0
- django_api_opentelemetry-0.1.0/pyproject.toml +71 -0
- django_api_opentelemetry-0.1.0/pyproject.toml.orig +62 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/__init__.py +8 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/_asgi.py +54 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/_config.py +133 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/_ninja.py +546 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/_runtime.py +511 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/_telemetry.py +626 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/apps.py +47 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/middleware.py +447 -0
- django_api_opentelemetry-0.1.0/src/django_api_opentelemetry/py.typed +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-api-opentelemetry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenTelemetry traces, metrics and exception logs for Django and Django Ninja
|
|
5
|
+
Classifier: Development Status :: 3 - Alpha
|
|
6
|
+
Classifier: Framework :: Django
|
|
7
|
+
Classifier: Framework :: Django :: 5.2
|
|
8
|
+
Classifier: Framework :: Django :: 6.0
|
|
9
|
+
Classifier: Framework :: Django :: 6.1
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: django>=5.2,<6.2
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.44,<2
|
|
16
|
+
Requires-Dist: django-ninja>=1.7,<1.8 ; extra == 'ninja'
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44,<2 ; extra == 'otlp'
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.44,<2 ; extra == 'otlp'
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Provides-Extra: ninja
|
|
21
|
+
Provides-Extra: otlp
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# django-api-opentelemetry
|
|
25
|
+
|
|
26
|
+
OpenTelemetry traces, metrics and exception logs for Django, with deeper
|
|
27
|
+
instrumentation for Django Ninja APIs.
|
|
28
|
+
|
|
29
|
+
> Tagged releases are validated in CI, then published to PyPI by the project's
|
|
30
|
+
> GitHub Actions workflow.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- One installed-app entry adds HTTP server spans and the
|
|
35
|
+
`http.server.request.duration` and `http.server.active_requests` metrics for
|
|
36
|
+
Django requests, using resolved route templates rather than raw paths.
|
|
37
|
+
- Django Ninja input-validation failures produce one warning event with the
|
|
38
|
+
validation count and route. Unexpected exceptions produce one correlated
|
|
39
|
+
error event with type, message and stack trace.
|
|
40
|
+
- Ninja operation spans cover input resolution, the bound endpoint and response
|
|
41
|
+
serialization.
|
|
42
|
+
- OTLP/HTTP export is opt-in and separate from recording. Existing tracer, meter
|
|
43
|
+
and logger providers remain application-owned.
|
|
44
|
+
- The standard OpenTelemetry Django instrumentation is detected so the package
|
|
45
|
+
does not create a second HTTP span or HTTP metric.
|
|
46
|
+
|
|
47
|
+
The package does **not** forward arbitrary Python application logs and does not
|
|
48
|
+
create an access log for every successful request. Its automatic log events are
|
|
49
|
+
limited to validation warnings and unexpected request exceptions.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
The base package supports plain Django without Ninja or the SDK/exporters:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install django-api-opentelemetry
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add the optional extras when the application uses Django Ninja or the bundled
|
|
60
|
+
OTLP export setup:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install "django-api-opentelemetry[ninja,otlp]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Supported versions:
|
|
67
|
+
|
|
68
|
+
- Python 3.12–3.14
|
|
69
|
+
- Django 5.2–6.1 (`>=5.2,<6.2`)
|
|
70
|
+
- Django Ninja 1.7 (`>=1.7,<1.8`, optional)
|
|
71
|
+
- OpenTelemetry API/SDK 1.44–1.x (`>=1.44,<2`)
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
Add the app. The installed app prepends its middleware when it is not already
|
|
76
|
+
present, registers Django's exception signal and activates supported Ninja
|
|
77
|
+
hooks:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
INSTALLED_APPS = [
|
|
81
|
+
# Existing applications...
|
|
82
|
+
"django_api_opentelemetry",
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Configuration is optional:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
DJANGO_NINJA_OTEL = {
|
|
90
|
+
"exclude": lambda request: request.path_info == "/health",
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For reliable export flushing on ASGI servers, wrap the Django ASGI application:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# asgi.py
|
|
98
|
+
from django.core.asgi import get_asgi_application
|
|
99
|
+
|
|
100
|
+
from django_api_opentelemetry import instrument_asgi
|
|
101
|
+
|
|
102
|
+
application = instrument_asgi(get_asgi_application())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`instrument_asgi()` adds a minimal lifespan wrapper. Native Django ASGI
|
|
106
|
+
applications do not implement lifespan, so the wrapper acknowledges startup
|
|
107
|
+
without initializing exporters, forwards HTTP and WebSocket scopes unchanged,
|
|
108
|
+
and flushes package-owned telemetry when the server sends lifespan shutdown.
|
|
109
|
+
|
|
110
|
+
The installed app still activates HTTP telemetry and Ninja hooks on its own; the
|
|
111
|
+
wrapper only adds a reliable shutdown path. This is recommended because Uvicorn
|
|
112
|
+
0.54 re-raises `SIGTERM` after draining the server, which bypasses `atexit`.
|
|
113
|
+
For other worker types, call the synchronous `shutdown()` from a worker-exit hook
|
|
114
|
+
after requests have drained:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from django_api_opentelemetry import shutdown
|
|
118
|
+
|
|
119
|
+
shutdown()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`shutdown()` waits for a bounded time, is safe to call more than once, and only
|
|
123
|
+
shuts down resources owned by this package. It does not intercept signals, and
|
|
124
|
+
the package cannot guarantee a flush on every possible server-termination path.
|
|
125
|
+
Application-owned providers are never closed.
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
All keys are optional. Invalid key names or value types raise
|
|
130
|
+
`django.core.exceptions.ImproperlyConfigured`; an invalid
|
|
131
|
+
`DJANGO_NINJA_OTEL_AUTO_CONFIGURE` value raises the same exception.
|
|
132
|
+
|
|
133
|
+
| Key | Default | Meaning |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| `tracing` | `True` | Enable package-created spans, including operation spans. |
|
|
136
|
+
| `metrics` | `True` | Enable HTTP measurements. |
|
|
137
|
+
| `logs` | `True` | Emit automatic validation and exception log events. |
|
|
138
|
+
| `operation_spans` | `True` | Add Ninja operation spans when tracing is enabled. |
|
|
139
|
+
| `http_instrumentation` | `True` | Allow package-owned HTTP server spans and metrics. Set `False` when another wrapper owns them. |
|
|
140
|
+
| `auto_configure` | Environment value, otherwise `False` | Configure OTLP/HTTP exporters. |
|
|
141
|
+
| `exclude` | `None` | Callable receiving `HttpRequest`; true suppresses all package telemetry for that request. |
|
|
142
|
+
| `tracer_provider` | `None` | Explicit tracer provider, otherwise the selected global provider. |
|
|
143
|
+
| `meter_provider` | `None` | Explicit meter provider, otherwise the selected global provider. |
|
|
144
|
+
| `logger_provider` | `None` | Explicit logger provider, otherwise the selected global provider. |
|
|
145
|
+
|
|
146
|
+
`DJANGO_NINJA_OTEL_AUTO_CONFIGURE=true` enables OTLP exporter setup unless the
|
|
147
|
+
Django setting sets `auto_configure` explicitly. Export setup is deliberately
|
|
148
|
+
opt-in: recording telemetry and deciding where it is sent are separate concerns,
|
|
149
|
+
matching the upstream FastAPI 0.143 change.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
export DJANGO_NINJA_OTEL_AUTO_CONFIGURE=true
|
|
153
|
+
export OTEL_SERVICE_NAME=my-api
|
|
154
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inside Compose, use the collector service address
|
|
158
|
+
`http://collector:4318` instead of `localhost`.
|
|
159
|
+
|
|
160
|
+
Exporter behavior:
|
|
161
|
+
|
|
162
|
+
- With no endpoint configured, the package installs no OTLP exporter and does
|
|
163
|
+
not guess a default endpoint.
|
|
164
|
+
- A base endpoint gains the signal path (`/v1/traces`, `/v1/metrics`,
|
|
165
|
+
`/v1/logs`); a signal-specific endpoint is used as written.
|
|
166
|
+
- Only the OTLP HTTP/protobuf protocol is supported.
|
|
167
|
+
- `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER` or `OTEL_LOGS_EXPORTER` set to
|
|
168
|
+
`none` disables that signal; other non-OTLP values are skipped with a warning.
|
|
169
|
+
- `OTEL_SDK_DISABLED=true` disables package telemetry.
|
|
170
|
+
- Standard endpoint, header, timeout and resource environment variables are
|
|
171
|
+
delegated to the official OpenTelemetry exporters.
|
|
172
|
+
|
|
173
|
+
### Providers and coexistence
|
|
174
|
+
|
|
175
|
+
Explicit providers in `DJANGO_NINJA_OTEL` take precedence. Otherwise the package
|
|
176
|
+
uses the global providers. With `auto_configure` enabled, it adds processors,
|
|
177
|
+
readers or exporters to an existing provider when that provider supports
|
|
178
|
+
registration, or installs an SDK provider when the global provider is still
|
|
179
|
+
unconfigured. Only package-owned components and providers are flushed and shut
|
|
180
|
+
down.
|
|
181
|
+
|
|
182
|
+
If the standard OpenTelemetry Django middleware is installed, the package detects
|
|
183
|
+
it and leaves HTTP spans and metrics to that integration while retaining Ninja
|
|
184
|
+
operation spans, validation warnings and exception events. For other HTTP
|
|
185
|
+
wrappers, set `http_instrumentation=False`; do not assume detection of every
|
|
186
|
+
vendor wrapper.
|
|
187
|
+
|
|
188
|
+
## Recording boundaries and privacy
|
|
189
|
+
|
|
190
|
+
With the default prepended middleware position, a buffered response is finished
|
|
191
|
+
after response middleware completes. For streaming responses, it is finished at
|
|
192
|
+
iterator exhaustion, failure or close. These are Django-observable boundaries,
|
|
193
|
+
not FastAPI's ASGI `send` boundary.
|
|
194
|
+
|
|
195
|
+
Validation warnings contain only `django_ninja.validation.error_count` and the
|
|
196
|
+
resolved `http.route`; invalid input is not serialized into the event. Unexpected
|
|
197
|
+
exception events can contain the exception message and stack trace. Redact
|
|
198
|
+
sensitive data at the OpenTelemetry processor/exporter layer or set
|
|
199
|
+
`logs=False` if those values must not leave the process. Error logs are emitted
|
|
200
|
+
independently of trace sampling.
|
|
201
|
+
|
|
202
|
+
## Development in Docker
|
|
203
|
+
|
|
204
|
+
Docker Engine/Desktop with Compose v2 and BuildKit is the only host requirement.
|
|
205
|
+
No host Python, `uv`, Ruff or ty installation is needed. Dependencies live in
|
|
206
|
+
`/opt/venv`; the checkout is mounted at `/workspace`.
|
|
207
|
+
|
|
208
|
+
Build the development image with host-matching ownership:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
docker compose build \
|
|
212
|
+
--build-arg LOCAL_UID="$(id -u)" \
|
|
213
|
+
--build-arg LOCAL_GID="$(id -g)" dev
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Start the demo server on `127.0.0.1:8000` only:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
docker compose up dev
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The demo command selects `tests.project.demo_settings` and runs the shared ASGI
|
|
223
|
+
project with reload enabled. `tests.project.asgi` already wraps the Django
|
|
224
|
+
application with `instrument_asgi(get_asgi_application())`, so the demo exercises
|
|
225
|
+
the lifespan shutdown path. Ordinary pytest runs use the baseline
|
|
226
|
+
`tests.project.settings`, which deliberately keeps the package out of
|
|
227
|
+
`INSTALLED_APPS`, so plain-Django tests remain a real condition. The demo
|
|
228
|
+
settings are not applied to pytest.
|
|
229
|
+
|
|
230
|
+
Run tests and checks:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
docker compose run --rm dev python -m pytest
|
|
234
|
+
docker compose run --rm dev bash scripts/check.sh all
|
|
235
|
+
docker compose run --rm dev python -m ruff check .
|
|
236
|
+
docker compose run --rm dev python -m ruff format --check .
|
|
237
|
+
docker compose run --rm dev ty check --python /opt/venv src
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`scripts/check.sh all` runs Ruff lint, Ruff format check, ty and pytest. The image
|
|
241
|
+
is built with a locked `uv sync` for the `ninja`/`otlp` extras and development
|
|
242
|
+
group. Rebuild after changing `pyproject.toml` or `uv.lock`; normal test commands
|
|
243
|
+
do not resync dependencies. Compatibility jobs build isolated environments with
|
|
244
|
+
`scripts/compat.sh` rather than modifying the committed lockfile. Final local
|
|
245
|
+
verification passed Ruff, format, ty and 227 tests in the default environment,
|
|
246
|
+
and the nine Python/Django matrix jobs plus four compatibility jobs. See
|
|
247
|
+
[task status](docs/tasks.md) for the evidence.
|
|
248
|
+
|
|
249
|
+
Build and verify distributions:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
docker build --target dist --output dist .
|
|
253
|
+
docker compose run --rm dev python scripts/check-dist.py --self-test
|
|
254
|
+
docker compose run --rm dev python scripts/check-dist.py dist
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The `dist` target exports exactly the wheel and sdist built inside the image.
|
|
258
|
+
`check-dist.py` runs `twine check`, installs the wheel into a clean temporary
|
|
259
|
+
environment outside `/workspace`, verifies the import resolves to the installed
|
|
260
|
+
distribution, rebuilds a wheel from the sdist and repeats the import check. The
|
|
261
|
+
directory must contain exactly one wheel and one sdist; remove stale artifacts
|
|
262
|
+
first.
|
|
263
|
+
|
|
264
|
+
Run the collector smoke test against the built wheel:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
docker compose --profile telemetry down -v
|
|
268
|
+
docker compose --profile telemetry up -d --wait collector
|
|
269
|
+
docker compose --profile telemetry run --rm smoke
|
|
270
|
+
docker compose --profile telemetry down -v
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The `telemetry` profile runs a one-shot volume initializer, then a non-root
|
|
274
|
+
collector writing all three signals to a shared volume. The smoke service sees
|
|
275
|
+
that output read-only, installs the wheel from `./dist` into a fresh temporary
|
|
276
|
+
virtual environment, starts Uvicorn and requests a success route, a Ninja
|
|
277
|
+
validation failure and an uncaught error. It asserts all three signals, trace/log
|
|
278
|
+
correlation, validation-input omission and both HTTP metrics.
|
|
279
|
+
|
|
280
|
+
The smoke check requires a fresh collector volume. If it reports stale output,
|
|
281
|
+
run `docker compose --profile telemetry down -v` before starting the collector
|
|
282
|
+
again. A repeated smoke run without that reset is expected to fail rather than
|
|
283
|
+
reuse old records. Collector diagnostics are available with:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
docker compose --profile telemetry logs collector
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
See [development and CI](docs/development-and-ci.md) for the full container,
|
|
290
|
+
compatibility-matrix and release workflow, and [task status](docs/tasks.md) for
|
|
291
|
+
current verification evidence.
|
|
292
|
+
|
|
293
|
+
## Deliberate limits
|
|
294
|
+
|
|
295
|
+
The first release does not include DRF, Channels/WebSockets, background-job
|
|
296
|
+
instrumentation, automatic forwarding of application logs, per-chunk spans or a
|
|
297
|
+
dashboard stack. Ninja hooks require Ninja 1.7 and are disabled with one
|
|
298
|
+
diagnostic on an unsupported version while ordinary Django telemetry continues.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# django-api-opentelemetry
|
|
2
|
+
|
|
3
|
+
OpenTelemetry traces, metrics and exception logs for Django, with deeper
|
|
4
|
+
instrumentation for Django Ninja APIs.
|
|
5
|
+
|
|
6
|
+
> Tagged releases are validated in CI, then published to PyPI by the project's
|
|
7
|
+
> GitHub Actions workflow.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- One installed-app entry adds HTTP server spans and the
|
|
12
|
+
`http.server.request.duration` and `http.server.active_requests` metrics for
|
|
13
|
+
Django requests, using resolved route templates rather than raw paths.
|
|
14
|
+
- Django Ninja input-validation failures produce one warning event with the
|
|
15
|
+
validation count and route. Unexpected exceptions produce one correlated
|
|
16
|
+
error event with type, message and stack trace.
|
|
17
|
+
- Ninja operation spans cover input resolution, the bound endpoint and response
|
|
18
|
+
serialization.
|
|
19
|
+
- OTLP/HTTP export is opt-in and separate from recording. Existing tracer, meter
|
|
20
|
+
and logger providers remain application-owned.
|
|
21
|
+
- The standard OpenTelemetry Django instrumentation is detected so the package
|
|
22
|
+
does not create a second HTTP span or HTTP metric.
|
|
23
|
+
|
|
24
|
+
The package does **not** forward arbitrary Python application logs and does not
|
|
25
|
+
create an access log for every successful request. Its automatic log events are
|
|
26
|
+
limited to validation warnings and unexpected request exceptions.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
The base package supports plain Django without Ninja or the SDK/exporters:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install django-api-opentelemetry
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add the optional extras when the application uses Django Ninja or the bundled
|
|
37
|
+
OTLP export setup:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install "django-api-opentelemetry[ninja,otlp]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Supported versions:
|
|
44
|
+
|
|
45
|
+
- Python 3.12–3.14
|
|
46
|
+
- Django 5.2–6.1 (`>=5.2,<6.2`)
|
|
47
|
+
- Django Ninja 1.7 (`>=1.7,<1.8`, optional)
|
|
48
|
+
- OpenTelemetry API/SDK 1.44–1.x (`>=1.44,<2`)
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
Add the app. The installed app prepends its middleware when it is not already
|
|
53
|
+
present, registers Django's exception signal and activates supported Ninja
|
|
54
|
+
hooks:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
# Existing applications...
|
|
59
|
+
"django_api_opentelemetry",
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Configuration is optional:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
DJANGO_NINJA_OTEL = {
|
|
67
|
+
"exclude": lambda request: request.path_info == "/health",
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For reliable export flushing on ASGI servers, wrap the Django ASGI application:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# asgi.py
|
|
75
|
+
from django.core.asgi import get_asgi_application
|
|
76
|
+
|
|
77
|
+
from django_api_opentelemetry import instrument_asgi
|
|
78
|
+
|
|
79
|
+
application = instrument_asgi(get_asgi_application())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`instrument_asgi()` adds a minimal lifespan wrapper. Native Django ASGI
|
|
83
|
+
applications do not implement lifespan, so the wrapper acknowledges startup
|
|
84
|
+
without initializing exporters, forwards HTTP and WebSocket scopes unchanged,
|
|
85
|
+
and flushes package-owned telemetry when the server sends lifespan shutdown.
|
|
86
|
+
|
|
87
|
+
The installed app still activates HTTP telemetry and Ninja hooks on its own; the
|
|
88
|
+
wrapper only adds a reliable shutdown path. This is recommended because Uvicorn
|
|
89
|
+
0.54 re-raises `SIGTERM` after draining the server, which bypasses `atexit`.
|
|
90
|
+
For other worker types, call the synchronous `shutdown()` from a worker-exit hook
|
|
91
|
+
after requests have drained:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from django_api_opentelemetry import shutdown
|
|
95
|
+
|
|
96
|
+
shutdown()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`shutdown()` waits for a bounded time, is safe to call more than once, and only
|
|
100
|
+
shuts down resources owned by this package. It does not intercept signals, and
|
|
101
|
+
the package cannot guarantee a flush on every possible server-termination path.
|
|
102
|
+
Application-owned providers are never closed.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
All keys are optional. Invalid key names or value types raise
|
|
107
|
+
`django.core.exceptions.ImproperlyConfigured`; an invalid
|
|
108
|
+
`DJANGO_NINJA_OTEL_AUTO_CONFIGURE` value raises the same exception.
|
|
109
|
+
|
|
110
|
+
| Key | Default | Meaning |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `tracing` | `True` | Enable package-created spans, including operation spans. |
|
|
113
|
+
| `metrics` | `True` | Enable HTTP measurements. |
|
|
114
|
+
| `logs` | `True` | Emit automatic validation and exception log events. |
|
|
115
|
+
| `operation_spans` | `True` | Add Ninja operation spans when tracing is enabled. |
|
|
116
|
+
| `http_instrumentation` | `True` | Allow package-owned HTTP server spans and metrics. Set `False` when another wrapper owns them. |
|
|
117
|
+
| `auto_configure` | Environment value, otherwise `False` | Configure OTLP/HTTP exporters. |
|
|
118
|
+
| `exclude` | `None` | Callable receiving `HttpRequest`; true suppresses all package telemetry for that request. |
|
|
119
|
+
| `tracer_provider` | `None` | Explicit tracer provider, otherwise the selected global provider. |
|
|
120
|
+
| `meter_provider` | `None` | Explicit meter provider, otherwise the selected global provider. |
|
|
121
|
+
| `logger_provider` | `None` | Explicit logger provider, otherwise the selected global provider. |
|
|
122
|
+
|
|
123
|
+
`DJANGO_NINJA_OTEL_AUTO_CONFIGURE=true` enables OTLP exporter setup unless the
|
|
124
|
+
Django setting sets `auto_configure` explicitly. Export setup is deliberately
|
|
125
|
+
opt-in: recording telemetry and deciding where it is sent are separate concerns,
|
|
126
|
+
matching the upstream FastAPI 0.143 change.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export DJANGO_NINJA_OTEL_AUTO_CONFIGURE=true
|
|
130
|
+
export OTEL_SERVICE_NAME=my-api
|
|
131
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Inside Compose, use the collector service address
|
|
135
|
+
`http://collector:4318` instead of `localhost`.
|
|
136
|
+
|
|
137
|
+
Exporter behavior:
|
|
138
|
+
|
|
139
|
+
- With no endpoint configured, the package installs no OTLP exporter and does
|
|
140
|
+
not guess a default endpoint.
|
|
141
|
+
- A base endpoint gains the signal path (`/v1/traces`, `/v1/metrics`,
|
|
142
|
+
`/v1/logs`); a signal-specific endpoint is used as written.
|
|
143
|
+
- Only the OTLP HTTP/protobuf protocol is supported.
|
|
144
|
+
- `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER` or `OTEL_LOGS_EXPORTER` set to
|
|
145
|
+
`none` disables that signal; other non-OTLP values are skipped with a warning.
|
|
146
|
+
- `OTEL_SDK_DISABLED=true` disables package telemetry.
|
|
147
|
+
- Standard endpoint, header, timeout and resource environment variables are
|
|
148
|
+
delegated to the official OpenTelemetry exporters.
|
|
149
|
+
|
|
150
|
+
### Providers and coexistence
|
|
151
|
+
|
|
152
|
+
Explicit providers in `DJANGO_NINJA_OTEL` take precedence. Otherwise the package
|
|
153
|
+
uses the global providers. With `auto_configure` enabled, it adds processors,
|
|
154
|
+
readers or exporters to an existing provider when that provider supports
|
|
155
|
+
registration, or installs an SDK provider when the global provider is still
|
|
156
|
+
unconfigured. Only package-owned components and providers are flushed and shut
|
|
157
|
+
down.
|
|
158
|
+
|
|
159
|
+
If the standard OpenTelemetry Django middleware is installed, the package detects
|
|
160
|
+
it and leaves HTTP spans and metrics to that integration while retaining Ninja
|
|
161
|
+
operation spans, validation warnings and exception events. For other HTTP
|
|
162
|
+
wrappers, set `http_instrumentation=False`; do not assume detection of every
|
|
163
|
+
vendor wrapper.
|
|
164
|
+
|
|
165
|
+
## Recording boundaries and privacy
|
|
166
|
+
|
|
167
|
+
With the default prepended middleware position, a buffered response is finished
|
|
168
|
+
after response middleware completes. For streaming responses, it is finished at
|
|
169
|
+
iterator exhaustion, failure or close. These are Django-observable boundaries,
|
|
170
|
+
not FastAPI's ASGI `send` boundary.
|
|
171
|
+
|
|
172
|
+
Validation warnings contain only `django_ninja.validation.error_count` and the
|
|
173
|
+
resolved `http.route`; invalid input is not serialized into the event. Unexpected
|
|
174
|
+
exception events can contain the exception message and stack trace. Redact
|
|
175
|
+
sensitive data at the OpenTelemetry processor/exporter layer or set
|
|
176
|
+
`logs=False` if those values must not leave the process. Error logs are emitted
|
|
177
|
+
independently of trace sampling.
|
|
178
|
+
|
|
179
|
+
## Development in Docker
|
|
180
|
+
|
|
181
|
+
Docker Engine/Desktop with Compose v2 and BuildKit is the only host requirement.
|
|
182
|
+
No host Python, `uv`, Ruff or ty installation is needed. Dependencies live in
|
|
183
|
+
`/opt/venv`; the checkout is mounted at `/workspace`.
|
|
184
|
+
|
|
185
|
+
Build the development image with host-matching ownership:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
docker compose build \
|
|
189
|
+
--build-arg LOCAL_UID="$(id -u)" \
|
|
190
|
+
--build-arg LOCAL_GID="$(id -g)" dev
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Start the demo server on `127.0.0.1:8000` only:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
docker compose up dev
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The demo command selects `tests.project.demo_settings` and runs the shared ASGI
|
|
200
|
+
project with reload enabled. `tests.project.asgi` already wraps the Django
|
|
201
|
+
application with `instrument_asgi(get_asgi_application())`, so the demo exercises
|
|
202
|
+
the lifespan shutdown path. Ordinary pytest runs use the baseline
|
|
203
|
+
`tests.project.settings`, which deliberately keeps the package out of
|
|
204
|
+
`INSTALLED_APPS`, so plain-Django tests remain a real condition. The demo
|
|
205
|
+
settings are not applied to pytest.
|
|
206
|
+
|
|
207
|
+
Run tests and checks:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
docker compose run --rm dev python -m pytest
|
|
211
|
+
docker compose run --rm dev bash scripts/check.sh all
|
|
212
|
+
docker compose run --rm dev python -m ruff check .
|
|
213
|
+
docker compose run --rm dev python -m ruff format --check .
|
|
214
|
+
docker compose run --rm dev ty check --python /opt/venv src
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`scripts/check.sh all` runs Ruff lint, Ruff format check, ty and pytest. The image
|
|
218
|
+
is built with a locked `uv sync` for the `ninja`/`otlp` extras and development
|
|
219
|
+
group. Rebuild after changing `pyproject.toml` or `uv.lock`; normal test commands
|
|
220
|
+
do not resync dependencies. Compatibility jobs build isolated environments with
|
|
221
|
+
`scripts/compat.sh` rather than modifying the committed lockfile. Final local
|
|
222
|
+
verification passed Ruff, format, ty and 227 tests in the default environment,
|
|
223
|
+
and the nine Python/Django matrix jobs plus four compatibility jobs. See
|
|
224
|
+
[task status](docs/tasks.md) for the evidence.
|
|
225
|
+
|
|
226
|
+
Build and verify distributions:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
docker build --target dist --output dist .
|
|
230
|
+
docker compose run --rm dev python scripts/check-dist.py --self-test
|
|
231
|
+
docker compose run --rm dev python scripts/check-dist.py dist
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The `dist` target exports exactly the wheel and sdist built inside the image.
|
|
235
|
+
`check-dist.py` runs `twine check`, installs the wheel into a clean temporary
|
|
236
|
+
environment outside `/workspace`, verifies the import resolves to the installed
|
|
237
|
+
distribution, rebuilds a wheel from the sdist and repeats the import check. The
|
|
238
|
+
directory must contain exactly one wheel and one sdist; remove stale artifacts
|
|
239
|
+
first.
|
|
240
|
+
|
|
241
|
+
Run the collector smoke test against the built wheel:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
docker compose --profile telemetry down -v
|
|
245
|
+
docker compose --profile telemetry up -d --wait collector
|
|
246
|
+
docker compose --profile telemetry run --rm smoke
|
|
247
|
+
docker compose --profile telemetry down -v
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `telemetry` profile runs a one-shot volume initializer, then a non-root
|
|
251
|
+
collector writing all three signals to a shared volume. The smoke service sees
|
|
252
|
+
that output read-only, installs the wheel from `./dist` into a fresh temporary
|
|
253
|
+
virtual environment, starts Uvicorn and requests a success route, a Ninja
|
|
254
|
+
validation failure and an uncaught error. It asserts all three signals, trace/log
|
|
255
|
+
correlation, validation-input omission and both HTTP metrics.
|
|
256
|
+
|
|
257
|
+
The smoke check requires a fresh collector volume. If it reports stale output,
|
|
258
|
+
run `docker compose --profile telemetry down -v` before starting the collector
|
|
259
|
+
again. A repeated smoke run without that reset is expected to fail rather than
|
|
260
|
+
reuse old records. Collector diagnostics are available with:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
docker compose --profile telemetry logs collector
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
See [development and CI](docs/development-and-ci.md) for the full container,
|
|
267
|
+
compatibility-matrix and release workflow, and [task status](docs/tasks.md) for
|
|
268
|
+
current verification evidence.
|
|
269
|
+
|
|
270
|
+
## Deliberate limits
|
|
271
|
+
|
|
272
|
+
The first release does not include DRF, Channels/WebSockets, background-job
|
|
273
|
+
instrumentation, automatic forwarding of application logs, per-chunk spans or a
|
|
274
|
+
dashboard stack. Ninja hooks require Ninja 1.7 and are disabled with one
|
|
275
|
+
diagnostic on an unsupported version while ordinary Django telemetry continues.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-api-opentelemetry"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "OpenTelemetry traces, metrics and exception logs for Django and Django Ninja"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"django>=5.2,<6.2",
|
|
13
|
+
"opentelemetry-api>=1.44,<2",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Django :: 5.2",
|
|
19
|
+
"Framework :: Django :: 6.0",
|
|
20
|
+
"Framework :: Django :: 6.1",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
ninja = ["django-ninja>=1.7,<1.8"]
|
|
29
|
+
otlp = [
|
|
30
|
+
"opentelemetry-exporter-otlp-proto-http>=1.44,<2",
|
|
31
|
+
"opentelemetry-sdk>=1.44,<2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"build>=1.2",
|
|
37
|
+
"django-stubs>=5.2",
|
|
38
|
+
"httpx>=0.27",
|
|
39
|
+
"opentelemetry-instrumentation-django[asgi]>=0.65b0",
|
|
40
|
+
"pytest>=8",
|
|
41
|
+
"pytest-asyncio>=0.24",
|
|
42
|
+
"pytest-django>=4.9",
|
|
43
|
+
"ruff>=0.9",
|
|
44
|
+
"twine>=6",
|
|
45
|
+
"ty>=0.0.1",
|
|
46
|
+
"uv_build>=0.13,<0.14",
|
|
47
|
+
"uvicorn>=0.34",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
DJANGO_SETTINGS_MODULE = "tests.project.settings"
|
|
52
|
+
minversion = "8.0"
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
addopts = "-ra"
|
|
55
|
+
asyncio_mode = "auto"
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
src = [
|
|
60
|
+
"src",
|
|
61
|
+
"tests",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E",
|
|
67
|
+
"F",
|
|
68
|
+
"I",
|
|
69
|
+
"UP",
|
|
70
|
+
"B",
|
|
71
|
+
]
|