django-lambda-tasks 0.8.0__tar.gz → 0.9.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_lambda_tasks-0.9.0/.github/dependabot.yml +19 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.github/workflows/ci.yml +6 -6
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.github/workflows/release.yml +6 -6
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/steering/product.md +20 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/steering/structure.md +3 -1
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/steering/tech.md +13 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.pre-commit-config.yaml +5 -5
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/PKG-INFO +9 -2
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/handler.py +18 -2
- django_lambda_tasks-0.9.0/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +48 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/models.py +10 -2
- django_lambda_tasks-0.9.0/lambda_tasks/tracing.py +255 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/pyproject.toml +13 -2
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_admin.py +27 -0
- django_lambda_tasks-0.9.0/tests/test_batch_worker.py +138 -0
- django_lambda_tasks-0.8.0/tests/test_batch_worker.py +0 -77
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.gitignore +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/README.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/README.md +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.8.0 → django_lambda_tasks-0.9.0}/tests/test_timeouts.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "monthly"
|
|
7
|
+
groups:
|
|
8
|
+
actions:
|
|
9
|
+
patterns:
|
|
10
|
+
- "*"
|
|
11
|
+
|
|
12
|
+
- package-ecosystem: "pre-commit"
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "monthly"
|
|
16
|
+
groups:
|
|
17
|
+
pre-commit:
|
|
18
|
+
patterns:
|
|
19
|
+
- "*"
|
|
@@ -6,7 +6,7 @@ on:
|
|
|
6
6
|
workflow_call:
|
|
7
7
|
|
|
8
8
|
env:
|
|
9
|
-
MINIMUM_PYTHON_VERSION: '3.
|
|
9
|
+
MINIMUM_PYTHON_VERSION: '3.11'
|
|
10
10
|
|
|
11
11
|
concurrency:
|
|
12
12
|
group: ${{ github.head_ref || github.run_id }}
|
|
@@ -17,9 +17,9 @@ jobs:
|
|
|
17
17
|
precommit:
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
21
|
- name: Install uv and set the python version
|
|
22
|
-
uses: astral-sh/setup-uv@
|
|
22
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
23
23
|
with:
|
|
24
24
|
python-version: ${{ env.MINIMUM_PYTHON_VERSION }}
|
|
25
25
|
- name: Run static code inspections
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
runs-on: ubuntu-latest
|
|
30
30
|
strategy:
|
|
31
31
|
matrix:
|
|
32
|
-
python-version: ["3.
|
|
32
|
+
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
33
33
|
services:
|
|
34
34
|
postgres:
|
|
35
35
|
image: postgres:17
|
|
@@ -45,9 +45,9 @@ jobs:
|
|
|
45
45
|
--health-timeout 5s
|
|
46
46
|
--health-retries 5
|
|
47
47
|
steps:
|
|
48
|
-
- uses: actions/checkout@
|
|
48
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
49
49
|
- name: Install uv and set the python version
|
|
50
|
-
uses: astral-sh/setup-uv@
|
|
50
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
51
51
|
with:
|
|
52
52
|
python-version: ${{ matrix.python-version }}
|
|
53
53
|
- name: Run tests
|
|
@@ -6,7 +6,7 @@ on:
|
|
|
6
6
|
- published
|
|
7
7
|
|
|
8
8
|
env:
|
|
9
|
-
MINIMUM_PYTHON_VERSION: '3.
|
|
9
|
+
MINIMUM_PYTHON_VERSION: '3.11'
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
ci:
|
|
@@ -18,15 +18,15 @@ jobs:
|
|
|
18
18
|
permissions:
|
|
19
19
|
contents: read
|
|
20
20
|
steps:
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
22
|
- name: Install Python ${{ env.MINIMUM_PYTHON_VERSION }}
|
|
23
|
-
uses: astral-sh/setup-uv@
|
|
23
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
24
24
|
with:
|
|
25
25
|
python-version: ${{ env.MINIMUM_PYTHON_VERSION }}
|
|
26
26
|
- name: Build the package
|
|
27
27
|
run: uv build
|
|
28
28
|
- name: 'Upload Artifact'
|
|
29
|
-
uses: actions/upload-artifact@
|
|
29
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
30
30
|
with:
|
|
31
31
|
name: python-dist
|
|
32
32
|
path: dist/
|
|
@@ -43,9 +43,9 @@ jobs:
|
|
|
43
43
|
id-token: write
|
|
44
44
|
steps:
|
|
45
45
|
- name: Download build artifacts
|
|
46
|
-
uses: actions/download-artifact@
|
|
46
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
47
47
|
with:
|
|
48
48
|
name: python-dist
|
|
49
49
|
path: dist
|
|
50
50
|
- name: Publish package distributions
|
|
51
|
-
uses: pypa/gh-action-pypi-publish@
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
@@ -31,6 +31,7 @@ Key modules:
|
|
|
31
31
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish, Batch submit, or local pool submit)
|
|
32
32
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution for development
|
|
33
33
|
- `handler.py` — AWS Lambda entry point + AWS Batch container entry point (`main()`)
|
|
34
|
+
- `tracing.py` — optional OpenTelemetry tracing (import-time setup, direct X-Ray OTLP export, SigV4-signed)
|
|
34
35
|
- `logging.py` — `task_logger` for invocation-scoped log output
|
|
35
36
|
- `settings.py` — lazy `LambdaTasksSettings` reading from Django settings, queue type helpers
|
|
36
37
|
|
|
@@ -158,6 +159,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
158
159
|
- Reads `LAMBDA_TASKS_MESSAGE` env var (the serialized task JSON, set by `submit_batch_job` via container overrides)
|
|
159
160
|
- Uses `AWS_BATCH_JOB_ID` as the `message_id` (falls back to UUID4 if not set)
|
|
160
161
|
- Constructs a synthetic SQS event and delegates to `handler()` — reuses the full cold-start and execution path
|
|
162
|
+
- Before returning, force-flushes and shuts down the global `TracerProvider` (if OTEL tracing is configured) so short-lived Batch jobs do not drop buffered spans
|
|
161
163
|
- Returns exit code 0 on success, 1 on failure
|
|
162
164
|
|
|
163
165
|
## Environment Loader
|
|
@@ -297,6 +299,24 @@ Relying on `atexit` alone loses this race in applications with a heavy shutdown
|
|
|
297
299
|
|
|
298
300
|
The checks are registered via `AppConfig.ready()` and tagged with `deploy=True` so they only fire during `--deploy` checks.
|
|
299
301
|
|
|
302
|
+
## OpenTelemetry Tracing (optional)
|
|
303
|
+
|
|
304
|
+
`lambda_tasks/tracing.py` adds OpenTelemetry tracing to the Lambda and Batch execution paths without the managed ADOT Lambda layer — the OTEL packages installed in the application image are the single source of instrumentation (reproducible-build friendly).
|
|
305
|
+
|
|
306
|
+
Setup lives in the handler module, **not** Django settings. `AwsLambdaInstrumentor` must wrap the handler named by `_HANDLER`/`ORIG_HANDLER` **before** the runtime invokes it, which is earlier than the cold-start `django.setup()` inside `handler()`. So `lambda_tasks.handler` calls `configure_tracing()` and `instrument_lambda_handler()` at **import time**, and the setup is driven purely by standard OTEL environment variables (settings are not configured yet at import time).
|
|
307
|
+
|
|
308
|
+
Behaviour:
|
|
309
|
+
- Gated on `OTEL_EXPORTER_OTLP_ENDPOINT` (or the per-signal `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`). Unset → everything is a no-op (local dev, tests, un-opted-in deployments).
|
|
310
|
+
- Builds a `TracerProvider` with an OTLP/HTTP span exporter via a `BatchSpanProcessor`. Spans go **directly** to the AWS X-Ray OTLP traces endpoint (`https://xray.<region>.amazonaws.com/v1/traces`) — no collector, no daemon, no Lambda layer.
|
|
311
|
+
- The exporter's `requests` session SigV4-signs each export with the ambient AWS credentials (task / execution role) via `botocore` — no API key to manage. Unsigned when no credentials are resolvable.
|
|
312
|
+
- Uses the AWS X-Ray id generator (`AwsXRayIdGenerator`) so the X-Ray backend accepts the trace IDs, and the X-Ray propagator (`AwsXRayLambdaPropagator` on Lambda, else `AwsXRayPropagator`). The X-Ray packages (`opentelemetry-sdk-extension-aws`, `opentelemetry-propagator-aws-xray`) are part of the `otel` extra.
|
|
313
|
+
- The OpenTelemetry / `requests` imports are **deferred into the functions** (module scope imports only `os`), so the OTEL packages remain a genuinely optional extra: importing `lambda_tasks.handler` with them uninstalled is a no-op, not an `ImportError`. The disabled path (endpoint unset) never reaches any OTEL import.
|
|
314
|
+
- Lambda path: also calls `AwsLambdaInstrumentor().instrument()` (only when `AWS_LAMBDA_FUNCTION_NAME` is set). Batch path (`main()`): the instrumentor is skipped (no handler to wrap); the provider alone is configured.
|
|
315
|
+
- Flush on shutdown: `main()` calls `TracerProvider.force_flush()` / `shutdown()` before the one-shot process exits so short-lived Batch jobs do not drop buffered spans. The Lambda path needs no manual flush — `AwsLambdaInstrumentor` force-flushes per invocation.
|
|
316
|
+
- Best-effort and observable: tracing must never take down the worker. `configure_tracing()` and `instrument_lambda_handler()` catch setup failures (missing OTEL packages, provider build errors) and log a warning instead of raising — important because they run at the handler module's import time. The `lambda_tasks.tracing` logger also surfaces otherwise-silent conditions at `INFO`/`WARNING`: tracing configured / already configured / disabled, handler instrumented or skipped (not on Lambda), no resolvable AWS region, and no resolvable AWS credentials (which would make exports unsigned and rejected by X-Ray).
|
|
317
|
+
|
|
318
|
+
`configure_tracing()` returns the installed provider (or `None` when disabled or already configured — it is idempotent). Install the OTEL packages via the `otel` extra; the consuming application sets `OTEL_EXPORTER_OTLP_ENDPOINT` and declares the X-Ray packages it wants.
|
|
319
|
+
|
|
300
320
|
## Logging
|
|
301
321
|
|
|
302
322
|
Import `task_logger` to emit log records that are automatically prefixed with the active `message_id`:
|
|
@@ -23,6 +23,7 @@ django-lambda-tasks/
|
|
|
23
23
|
│ ├── local_executor.py # ProcessPoolExecutor for async local task execution
|
|
24
24
|
│ ├── tasks.py # Built-in tasks (cleanup_task_records, submit_batch_job)
|
|
25
25
|
│ ├── timeouts.py # TimeoutContext implementation
|
|
26
|
+
│ ├── tracing.py # Optional OpenTelemetry tracing (import-time setup, X-Ray OTLP export)
|
|
26
27
|
│ └── migrations/ # Django migrations for TaskRecord
|
|
27
28
|
├── tests/ # pytest test suite
|
|
28
29
|
│ ├── conftest.py
|
|
@@ -43,7 +44,8 @@ django-lambda-tasks/
|
|
|
43
44
|
- `decorators.py` — defines `LambdaTaskWrapper` and `@lambda_task` decorator factory; enforces kwargs-only at decoration time; validates timeouts (> 0, soft < hard) at decoration time; upper-bound timeout validation deferred to execution time based on queue type
|
|
44
45
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, execute eagerly, or submit to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
45
46
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
|
|
46
|
-
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures`. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event,
|
|
47
|
+
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures`. At **import time** (before any invocation) it calls `configure_tracing()` and `instrument_lambda_handler()` from `tracing.py` so the Lambda instrumentor can wrap the handler before the runtime invokes it. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, delegates to `handler()`, then force-flushes and shuts down the global `TracerProvider` (if any) before the one-shot process exits
|
|
48
|
+
- `tracing.py` — optional OpenTelemetry tracing, driven purely by standard OTEL env vars (no Django settings) so the package stays generic; no-op unless `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) is set. All OpenTelemetry / `requests` imports are **deferred into the functions** (only `os` is imported at module scope) so the OTEL packages are a genuinely optional extra — importing `lambda_tasks.handler` without them is a no-op, not an `ImportError`. `configure_tracing()` installs a global `TracerProvider` using the X-Ray id generator (`AwsXRayIdGenerator`) with an OTLP/HTTP exporter whose `requests` session SigV4-signs each export to the AWS X-Ray traces endpoint (unsigned when no credentials resolve), and sets the X-Ray propagator (`AwsXRayLambdaPropagator` on Lambda, else `AwsXRayPropagator`). `instrument_lambda_handler()` calls `AwsLambdaInstrumentor().instrument()` only when tracing is enabled and `AWS_LAMBDA_FUNCTION_NAME` is set (Lambda path); skipped on the Batch path. `flush_tracing()` force-flushes and shuts down the provider (gated; used by the Batch entry point)
|
|
47
49
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
48
50
|
- `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
|
|
49
51
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
@@ -17,6 +17,19 @@ inclusion: always
|
|
|
17
17
|
| `boto3` | `>= 1.42.69` | AWS SDK — SQS publishing, Lambda integration |
|
|
18
18
|
| `pydantic` | `>= 2.12.5` | Task argument serialization via `SQSLambdaTaskMessage` model |
|
|
19
19
|
|
|
20
|
+
## Optional Dependencies
|
|
21
|
+
|
|
22
|
+
Installed via the `otel` extra (`pip install django-lambda-tasks[otel]`). Enables OpenTelemetry tracing in `lambda_tasks/tracing.py`; the library stays generic without them. SigV4 signing reuses `botocore` (already a boto3 transitive dependency) — no extra signing package.
|
|
23
|
+
|
|
24
|
+
| Package | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `opentelemetry-sdk` | `TracerProvider`, `BatchSpanProcessor` |
|
|
27
|
+
| `opentelemetry-exporter-otlp-proto-http` | OTLP/HTTP span exporter (no gRPC, no meta-package) |
|
|
28
|
+
| `opentelemetry-instrumentation-aws-lambda` | Wraps the Lambda handler (`AwsLambdaInstrumentor`) |
|
|
29
|
+
| `opentelemetry-sdk-extension-aws` | X-Ray id generator (`AwsXRayIdGenerator`) |
|
|
30
|
+
| `opentelemetry-propagator-aws-xray` | X-Ray trace propagator |
|
|
31
|
+
| `requests` | OTLP/HTTP transport session (SigV4-signed via `botocore`) |
|
|
32
|
+
|
|
20
33
|
## Dev Dependencies
|
|
21
34
|
|
|
22
35
|
| Package | Version | Purpose |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/adamchainz/django-upgrade
|
|
3
|
-
rev: 1.
|
|
3
|
+
rev: 1.33.0
|
|
4
4
|
hooks:
|
|
5
5
|
- id: django-upgrade
|
|
6
6
|
args: [--target-version, "5.2"]
|
|
@@ -13,17 +13,17 @@ repos:
|
|
|
13
13
|
- id: mixed-line-ending
|
|
14
14
|
- id: trailing-whitespace
|
|
15
15
|
- repo: https://github.com/pycqa/isort
|
|
16
|
-
rev:
|
|
16
|
+
rev: 9.0.2
|
|
17
17
|
hooks:
|
|
18
18
|
- id: isort
|
|
19
19
|
- repo: https://github.com/asottile/pyupgrade
|
|
20
|
-
rev: v3.
|
|
20
|
+
rev: v3.22.0
|
|
21
21
|
hooks:
|
|
22
22
|
- id: pyupgrade
|
|
23
23
|
language: python
|
|
24
|
-
args: [--
|
|
24
|
+
args: [--py311-plus]
|
|
25
25
|
- repo: https://github.com/ambv/black
|
|
26
|
-
rev: 26.
|
|
26
|
+
rev: 26.10.1
|
|
27
27
|
hooks:
|
|
28
28
|
- id: black
|
|
29
29
|
language: python
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
7
7
|
Requires-Dist: boto3
|
|
8
8
|
Requires-Dist: django
|
|
9
9
|
Requires-Dist: pydantic
|
|
10
10
|
Requires-Dist: redis
|
|
11
|
+
Provides-Extra: otel
|
|
12
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'otel'
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-aws-lambda; extra == 'otel'
|
|
14
|
+
Requires-Dist: opentelemetry-propagator-aws-xray; extra == 'otel'
|
|
15
|
+
Requires-Dist: opentelemetry-sdk; extra == 'otel'
|
|
16
|
+
Requires-Dist: opentelemetry-sdk-extension-aws; extra == 'otel'
|
|
17
|
+
Requires-Dist: requests; extra == 'otel'
|
|
11
18
|
Description-Content-Type: text/markdown
|
|
12
19
|
|
|
13
20
|
# Django Lambda Tasks
|
|
@@ -26,9 +26,21 @@ from django.apps import apps
|
|
|
26
26
|
|
|
27
27
|
from lambda_tasks.environment_loader import resolve_environment
|
|
28
28
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
29
|
+
from lambda_tasks.tracing import (
|
|
30
|
+
configure_tracing,
|
|
31
|
+
flush_tracing,
|
|
32
|
+
instrument_lambda_handler,
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
logger = logging.getLogger(__name__)
|
|
31
36
|
|
|
37
|
+
# Initialise tracing at import time so the Lambda instrumentor can wrap the
|
|
38
|
+
# handler before the first invocation (which is earlier than the cold-start
|
|
39
|
+
# django.setup() inside handler()). No-op unless OTEL_EXPORTER_OTLP_ENDPOINT
|
|
40
|
+
# is set.
|
|
41
|
+
configure_tracing()
|
|
42
|
+
instrument_lambda_handler()
|
|
43
|
+
|
|
32
44
|
_cold_start_done: bool = False
|
|
33
45
|
|
|
34
46
|
|
|
@@ -169,10 +181,14 @@ def main() -> int:
|
|
|
169
181
|
context=None,
|
|
170
182
|
)
|
|
171
183
|
|
|
184
|
+
# Flush any buffered spans before the one-shot process exits (no-op unless
|
|
185
|
+
# tracing is enabled — this never imports OTEL otherwise).
|
|
186
|
+
flush_tracing()
|
|
187
|
+
|
|
172
188
|
if result["batchItemFailures"]:
|
|
173
189
|
return 1
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
else:
|
|
191
|
+
return 0
|
|
176
192
|
|
|
177
193
|
|
|
178
194
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 6.0.3 on 2026-08-18 09:47
|
|
2
|
+
|
|
3
|
+
import django.contrib.postgres.indexes
|
|
4
|
+
import django.db.models.functions.comparison
|
|
5
|
+
import django.db.models.functions.text
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("lambda_tasks", "0003_taskrecord_taskrecord_kwargs_trgm"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.RemoveIndex(
|
|
17
|
+
model_name="taskrecord",
|
|
18
|
+
name="taskrecord_kwargs_trgm",
|
|
19
|
+
),
|
|
20
|
+
migrations.AddIndex(
|
|
21
|
+
model_name="taskrecord",
|
|
22
|
+
index=django.contrib.postgres.indexes.GinIndex(
|
|
23
|
+
django.contrib.postgres.indexes.OpClass(
|
|
24
|
+
django.db.models.functions.text.Upper(
|
|
25
|
+
django.db.models.functions.comparison.Cast(
|
|
26
|
+
"id", output_field=models.TextField()
|
|
27
|
+
)
|
|
28
|
+
),
|
|
29
|
+
name="gin_trgm_ops",
|
|
30
|
+
),
|
|
31
|
+
name="taskrecord_id_trgm",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
migrations.AddIndex(
|
|
35
|
+
model_name="taskrecord",
|
|
36
|
+
index=django.contrib.postgres.indexes.GinIndex(
|
|
37
|
+
django.contrib.postgres.indexes.OpClass(
|
|
38
|
+
django.db.models.functions.text.Upper(
|
|
39
|
+
django.db.models.functions.comparison.Cast(
|
|
40
|
+
"kwargs", output_field=models.TextField()
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
name="gin_trgm_ops",
|
|
44
|
+
),
|
|
45
|
+
name="taskrecord_kwargs_trgm",
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
]
|
|
@@ -13,7 +13,7 @@ from django.core.cache import caches
|
|
|
13
13
|
from django.core.exceptions import ImproperlyConfigured
|
|
14
14
|
from django.db import models, transaction
|
|
15
15
|
from django.db.models import Q
|
|
16
|
-
from django.db.models.functions import Cast
|
|
16
|
+
from django.db.models.functions import Cast, Upper
|
|
17
17
|
from django.utils.module_loading import import_string
|
|
18
18
|
from django.utils.timezone import now
|
|
19
19
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -78,7 +78,15 @@ class TaskRecord(models.Model):
|
|
|
78
78
|
models.Index(fields=["-start_time"]),
|
|
79
79
|
GinIndex(
|
|
80
80
|
OpClass(
|
|
81
|
-
Cast("
|
|
81
|
+
Upper(Cast("id", output_field=models.TextField())),
|
|
82
|
+
name="gin_trgm_ops",
|
|
83
|
+
),
|
|
84
|
+
name="taskrecord_id_trgm",
|
|
85
|
+
),
|
|
86
|
+
GinIndex(
|
|
87
|
+
OpClass(
|
|
88
|
+
Upper(Cast("kwargs", output_field=models.TextField())),
|
|
89
|
+
name="gin_trgm_ops",
|
|
82
90
|
),
|
|
83
91
|
name="taskrecord_kwargs_trgm",
|
|
84
92
|
),
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Optional OpenTelemetry tracing for the Lambda and Batch execution paths.
|
|
2
|
+
|
|
3
|
+
Initialised at import time of ``lambda_tasks.handler`` (before the Lambda
|
|
4
|
+
runtime invokes the handler, which is earlier than ``django.setup()``). Driven
|
|
5
|
+
entirely by standard OTEL environment variables so this package stays generic:
|
|
6
|
+
the consuming application declares the OTEL dependencies and sets
|
|
7
|
+
``OTEL_EXPORTER_OTLP_ENDPOINT`` to the regional AWS X-Ray OTLP traces endpoint
|
|
8
|
+
(``https://xray.<region>.amazonaws.com/v1/traces``).
|
|
9
|
+
|
|
10
|
+
Spans are exported directly to the X-Ray endpoint — no collector, no daemon,
|
|
11
|
+
no Lambda layer. Requests are SigV4-signed with the ambient AWS credentials
|
|
12
|
+
(the task / execution role), so there is no API key to manage. When no
|
|
13
|
+
credentials are resolvable (local dev / tests) the request is left unsigned.
|
|
14
|
+
|
|
15
|
+
The OpenTelemetry and ``requests`` imports are deferred into the functions
|
|
16
|
+
below so they are only reached when tracing is enabled (a non-empty
|
|
17
|
+
``OTEL_EXPORTER_OTLP_ENDPOINT`` / ``OTEL_EXPORTER_OTLP_TRACES_ENDPOINT``).
|
|
18
|
+
This keeps the OTEL packages a genuinely optional extra: importing
|
|
19
|
+
``lambda_tasks.handler`` without them installed is a no-op, not an
|
|
20
|
+
``ImportError``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import requests
|
|
29
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_XRAY_SIGV4_SERVICE_NAME = "xray"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tracing_is_enabled() -> bool:
|
|
37
|
+
"""Whether an OTLP traces endpoint has been configured.
|
|
38
|
+
|
|
39
|
+
Either the base ``OTEL_EXPORTER_OTLP_ENDPOINT`` or the per-signal
|
|
40
|
+
``OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`` turns it on.
|
|
41
|
+
"""
|
|
42
|
+
return bool(
|
|
43
|
+
os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip()
|
|
44
|
+
or os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "").strip()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _running_on_lambda() -> bool:
|
|
49
|
+
return bool(os.environ.get("AWS_LAMBDA_FUNCTION_NAME"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_signing_session() -> "requests.Session":
|
|
53
|
+
import requests
|
|
54
|
+
import requests.auth
|
|
55
|
+
from botocore.auth import SigV4Auth
|
|
56
|
+
from botocore.awsrequest import AWSRequest
|
|
57
|
+
from botocore.session import Session as BotocoreSession
|
|
58
|
+
|
|
59
|
+
class _SigV4Auth(requests.auth.AuthBase):
|
|
60
|
+
"""SigV4-signs OTLP exports to the X-Ray endpoint."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self, *, service_name: str, region: str, session: BotocoreSession
|
|
64
|
+
) -> None:
|
|
65
|
+
self._service_name = service_name
|
|
66
|
+
self._region = region
|
|
67
|
+
self._session = session
|
|
68
|
+
|
|
69
|
+
def __call__(self, r: "requests.PreparedRequest") -> "requests.PreparedRequest":
|
|
70
|
+
credentials = self._session.get_credentials()
|
|
71
|
+
|
|
72
|
+
if credentials is None:
|
|
73
|
+
logger.warning(
|
|
74
|
+
"No AWS credentials resolvable; sending OTLP span export to "
|
|
75
|
+
"%s unsigned. The X-Ray endpoint will reject unsigned "
|
|
76
|
+
"requests, so spans will be dropped.",
|
|
77
|
+
r.url,
|
|
78
|
+
)
|
|
79
|
+
return r
|
|
80
|
+
|
|
81
|
+
aws_request = AWSRequest(
|
|
82
|
+
method=r.method,
|
|
83
|
+
url=r.url,
|
|
84
|
+
data=r.body,
|
|
85
|
+
headers={"Content-Type": "application/x-protobuf"},
|
|
86
|
+
)
|
|
87
|
+
SigV4Auth(credentials, self._service_name, self._region).add_auth(
|
|
88
|
+
aws_request
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
r.headers.update(dict(aws_request.headers))
|
|
92
|
+
|
|
93
|
+
return r
|
|
94
|
+
|
|
95
|
+
region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", ""))
|
|
96
|
+
|
|
97
|
+
if not region:
|
|
98
|
+
logger.warning(
|
|
99
|
+
"No AWS region resolvable from AWS_REGION / AWS_DEFAULT_REGION; "
|
|
100
|
+
"SigV4 signing of OTLP span exports may fail."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
session = requests.Session()
|
|
104
|
+
session.auth = _SigV4Auth(
|
|
105
|
+
service_name=_XRAY_SIGV4_SERVICE_NAME,
|
|
106
|
+
region=region,
|
|
107
|
+
session=BotocoreSession(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return session
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _build_tracer_provider() -> "TracerProvider":
|
|
114
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
115
|
+
OTLPSpanExporter,
|
|
116
|
+
)
|
|
117
|
+
from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator
|
|
118
|
+
from opentelemetry.sdk.resources import Resource
|
|
119
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
120
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
121
|
+
|
|
122
|
+
service_name = os.environ.get(
|
|
123
|
+
"OTEL_SERVICE_NAME",
|
|
124
|
+
os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "lambda-tasks"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
tracer_provider = TracerProvider(
|
|
128
|
+
resource=Resource.create(attributes={"service.name": service_name}),
|
|
129
|
+
id_generator=AwsXRayIdGenerator(),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
tracer_provider.add_span_processor(
|
|
133
|
+
BatchSpanProcessor(
|
|
134
|
+
span_exporter=OTLPSpanExporter(session=_build_signing_session())
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return tracer_provider
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _set_xray_propagator() -> None:
|
|
141
|
+
from opentelemetry import propagate
|
|
142
|
+
from opentelemetry.propagators.aws import (
|
|
143
|
+
AwsXRayLambdaPropagator,
|
|
144
|
+
AwsXRayPropagator,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
propagate.set_global_textmap(
|
|
148
|
+
AwsXRayLambdaPropagator() if _running_on_lambda() else AwsXRayPropagator()
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def configure_tracing() -> "TracerProvider | None":
|
|
153
|
+
"""Install the global tracer provider. No-op unless enabled.
|
|
154
|
+
|
|
155
|
+
Returns the provider so callers (e.g. the Batch entry point) can flush it
|
|
156
|
+
on shutdown, or ``None`` when tracing is disabled or already configured.
|
|
157
|
+
|
|
158
|
+
Tracing is best-effort: if it is enabled but the OTEL packages are missing
|
|
159
|
+
or the provider cannot be built, the failure is logged and ``None`` is
|
|
160
|
+
returned rather than propagated — a broken observability setup must not
|
|
161
|
+
take down the worker (this runs at import time of the handler module).
|
|
162
|
+
"""
|
|
163
|
+
if not tracing_is_enabled():
|
|
164
|
+
logger.debug("OTEL tracing disabled (no OTLP endpoint configured).")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
from opentelemetry import trace
|
|
169
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
170
|
+
|
|
171
|
+
if isinstance(trace.get_tracer_provider(), TracerProvider):
|
|
172
|
+
logger.info(
|
|
173
|
+
"A TracerProvider is already installed; skipping configuration."
|
|
174
|
+
)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
tracer_provider = _build_tracer_provider()
|
|
178
|
+
trace.set_tracer_provider(tracer_provider)
|
|
179
|
+
_set_xray_propagator()
|
|
180
|
+
except ImportError:
|
|
181
|
+
logger.warning(
|
|
182
|
+
"OTEL tracing is enabled but the OpenTelemetry packages are not "
|
|
183
|
+
"installed; tracing is disabled. Install the 'otel' extra to "
|
|
184
|
+
"enable it.",
|
|
185
|
+
exc_info=True,
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
except Exception:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"Failed to configure OTEL tracing; continuing without it.",
|
|
191
|
+
exc_info=True,
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
logger.info("OTEL tracing configured (exporting spans to the OTLP endpoint).")
|
|
196
|
+
return tracer_provider
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def instrument_lambda_handler() -> None:
|
|
200
|
+
"""Wrap the Lambda handler for tracing. No-op unless enabled.
|
|
201
|
+
|
|
202
|
+
Must be called at import time of the handler module, before the runtime
|
|
203
|
+
invokes the handler.
|
|
204
|
+
"""
|
|
205
|
+
if not tracing_is_enabled():
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if not _running_on_lambda():
|
|
209
|
+
logger.debug(
|
|
210
|
+
"Not running on Lambda (AWS_LAMBDA_FUNCTION_NAME unset); "
|
|
211
|
+
"skipping AwsLambdaInstrumentor."
|
|
212
|
+
)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
from opentelemetry.instrumentation.aws_lambda import (
|
|
217
|
+
AwsLambdaInstrumentor,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
AwsLambdaInstrumentor().instrument()
|
|
221
|
+
except Exception:
|
|
222
|
+
logger.warning(
|
|
223
|
+
"Failed to instrument the Lambda handler for tracing; "
|
|
224
|
+
"continuing without handler spans.",
|
|
225
|
+
exc_info=True,
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
logger.info("AwsLambdaInstrumentor installed; Lambda handler is traced.")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def flush_tracing() -> None:
|
|
233
|
+
"""Force-flush and shut down the global tracer provider. No-op unless enabled.
|
|
234
|
+
|
|
235
|
+
Called at the end of the one-shot Batch entry point so short-lived jobs do
|
|
236
|
+
not drop buffered spans. Gated on ``tracing_is_enabled()`` so the OTEL
|
|
237
|
+
imports are never reached when tracing is disabled (or the packages are not
|
|
238
|
+
installed).
|
|
239
|
+
"""
|
|
240
|
+
if not tracing_is_enabled():
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
from opentelemetry import trace
|
|
244
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
245
|
+
|
|
246
|
+
provider = trace.get_tracer_provider()
|
|
247
|
+
if isinstance(provider, TracerProvider):
|
|
248
|
+
logger.debug("Flushing and shutting down the TracerProvider before exit.")
|
|
249
|
+
provider.force_flush()
|
|
250
|
+
provider.shutdown()
|
|
251
|
+
else:
|
|
252
|
+
logger.debug(
|
|
253
|
+
"Tracing is enabled but no TracerProvider is installed; "
|
|
254
|
+
"nothing to flush."
|
|
255
|
+
)
|
|
@@ -7,10 +7,10 @@ packages = ["lambda_tasks"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "django-lambda-tasks"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.9.0"
|
|
11
11
|
description = "Run async tasks in a lambda function"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"boto3",
|
|
16
16
|
"django",
|
|
@@ -19,6 +19,16 @@ dependencies = [
|
|
|
19
19
|
"awslambdaric",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
otel = [
|
|
24
|
+
"requests",
|
|
25
|
+
"opentelemetry-sdk",
|
|
26
|
+
"opentelemetry-exporter-otlp-proto-http",
|
|
27
|
+
"opentelemetry-instrumentation-aws-lambda",
|
|
28
|
+
"opentelemetry-sdk-extension-aws",
|
|
29
|
+
"opentelemetry-propagator-aws-xray",
|
|
30
|
+
]
|
|
31
|
+
|
|
22
32
|
[dependency-groups]
|
|
23
33
|
dev = [
|
|
24
34
|
"hypothesis",
|
|
@@ -31,6 +41,7 @@ dev = [
|
|
|
31
41
|
"pytest-cov",
|
|
32
42
|
"ty",
|
|
33
43
|
"psycopg",
|
|
44
|
+
"django-lambda-tasks[otel]",
|
|
34
45
|
]
|
|
35
46
|
|
|
36
47
|
[tool.ty.rules]
|