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.
@@ -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
+ ]